3 ноября 2017

Создание iOS-приложений с использованием RxSwift и MVVM-C на практике. Часть 1

Наш iOS-разработчик Дмитрий продолжает делиться с нами своим опытом и знаниями.

В этой статье я опишу, как можно создать мобильное приложение с использованием фреймворка RxSwift, основываясь на архитектуре MVVM-C. Думаю, эта статья будет полезна как опытным разработчикам, так и начинающим. Существует множество путей создания iOS-приложений, и начинающему разработчику особенно трудно выбрать один из них.

Сразу оговорюсь: идеального, наилучшего подхода просто не существует. У каждого есть свои плюсы и минусы. Но пройдя путь от MVC Apple, который она активно продвигала с 2007 года, до современных VIPER и MVVM через MVP и прочие, я для себя собрал практики и подходы, которые позволяют создавать максимально гибкие, надежные приложения.

Теперь, когда вы (я надеюсь) заинтересовались, давайте ответим на несколько основных вопросов:

1. Почему именно RxSwift?

Ответ: На самом деле причин, по которым его можно рекомендовать, множество. Самые главные из них:

  1. Он позволяет максимально чисто, просто, без лишнего кода описать необходимую логику, а также ее разделить.
  2. Идеально совместим с MVVM. Без этого фреймворка вам бы пришлось использовать сторонние инструменты или писать свои, чтобы связать View и ViewModel, а здесь есть множество готовых решений для этого.
  3. Поддержка и развитие. RxSwift активно совершенствуется. Возможно, когда-нибудь в будущем мы увидим его официальную версию, как, например, Apple поступила с Codable & Decodable — напомню, что этот функционал появился из одного из pull-request’ов.
  4. Изучая RxSwift, вы инвестируете в свое развитие. Подобная технология существует и для Java, и для JavaScript. Базовые знания этой технологии, если вы пойдете дальше, помогут вам в написании кроссплатформенных приложений на React-Native, например.

Но все же мир и особенно то, что создано людьми, не идеально. Минусы все же есть:

— Требует достаточно большой теоретической подготовки. RxSwift — это целый язык в языке. Прежде чем приступить к его использованию, я настоятельно рекомендую прочитать замечательную книгу Ray Wenderlich — https://store.raywenderlich.com/products/rxswift

— Зависимость. Используя любой фреймворк в дополнение к системным, вы увеличиваете свою зависимость от других разработчиков. Вам нужно быть готовым менять код, искать помощи на официальном форуме, если вдруг по непонятной вам причине ваш код не работает, и т.д.

2. Почему MVVM и что такое С в конце?

Ответ: На данном этапе развития технологий наиболее популярные архитектуры VIPER, MVVM и MVP. VIPER — достаточно громоздкая, хоть и четко разделяющая логику архитектура. Ее очень сложно объяснить новичку, и в большинстве случаев количество создаваемых объектов неоправданно. MVP — развитие классического MVC, лишенная ряда недостатков своего родителя. Она хорошо подходит, если вы не используете или принципиально не хотите использовать сторонние фреймворки. Но по скорости создания приложений и по гибкости она уступает MVVM. Также из своей практики могу сказать, что легче объяснить, как разделять логику между элементами, именно в MVVM. И с ним же проще создавать Unit-тесты.

Что же такое C в конце? За ним скрывается новый тип элементов — Coordinator. Классического MVVM недостаточно, чтобы организовать прозрачную логику перехода от View к последующему, скоординировать их взаимодействие. Именно этим он и будет заниматься.

Еще немного теории и сразу к практике

Так что же из себя представляет MVVM-C? Давайте попробуем продемонстрировать идеологию этой архитектуры на схеме:

Обо всем по порядку:

  1. Coordinator. На данной схеме кажется, что он стоит отдельно, в стороне от действующих лиц. На самом деле это не так. Полностью его роль и назначение раскроется, когда мы взглянем на схему более высокого уровня. А пока вам достаточно знать, что Coordinator инициализирует, хранит ссылки на View и ViewModel, связывает их, управляет их жизненным циклом. Проще говоря, координирует их.
  2. View. Хочу сразу сказать, что к этой категории мы также будем относить и все ViewController’ы. Для нас они будут просто View. В архитектуре MVVM у View всего 2 задачи: сообщать, что с ней «сделал» пользователь своей ViewModel (далее VM), и принимать сообщения от нее же о необходимости обновить себя или свои дочерние элементы (например, текст, цвет, видимость дочернего индикатора загрузки и пр.). Постарайтесь запомнить: View НЕ ПРИНИМАЕТ самостоятельных решений.
  3. ViewModel. Включает в себя все, что можно назвать бизнес-логикой. На этом стоит остановиться более подробно. В предложенной реализации предполагается, что VM должна иметь входные данные (Input) и выходные данные (Output). Например, наш любимый пользователь нажал на кнопку или ввел текст — это входные данные для VM. А если согласно этим входным данным, например, или по любой другой причине VM необходимо поменять состояние кнопки у своей View — то это выходные данные.

Давайте не будем откладывать в долгий ящик практику и сразу попробуем имплементировать описанную архитектуру приложения на практике.

Вам необходимо создать просто Single-Page проект и интегрировать в него RxSwift. Если у вас возникли сложности с этим, обратитесь к книге https://store.raywenderlich.com/products/rxswift , которую я уже рекомендовал прочитать выше.

У нас уже есть ViewController. Нам не хватает первого Coordinator’a. Как правило, координатор верхнего уровня называют AppCoordinator. Но прежде чем его создать, давайте объявим протокол, которому должны соответствовать все координаторы:

import Foundation
import RxSwift
import UIKit
 
protocol CoordinatorProtocol {
    func start(from viewController: UIViewController) -> Observable<Void>
    func coordinate(to coordinator: CoordinatorProtocol, from viewController: UIViewController) -> Observable<Void>
}

В нем всего 2 функции: первая должна содержать логику «разворачивания» координатора, вторая — логику перехода между ними. В общем виде, если нет необходимости имплементировать дополнительную логику, содержание второй функции будет следующим:

func coordinate(to coordinator: CoordinatorProtocol, from viewController: UIViewController) -> Observable<Void> {
    return coordinator.start(from: viewController)
}

Хочу обратить ваше внимание на аргумент, который возвращает функция start (Observable). Этот аргумент мы будем использовать для того, чтобы уведомить заинтересованных слушателей (координаторов) о завершении показа корневого View.

Итак, создадим наш AppCoordinator:

import Foundation
import RxSwift
import UIKit
 
class AppCoordinator: CoordinatorProtocol {
 
    // MARK: Public Properties
 
    lazy var loginCoordinator = LoginCoordinator()
 
    // MARK: Private Properties
 
    private let disposeBag = DisposeBag()
 
    // MARK: CoordinatorProtocol Methods
 
    func start(from viewController: UIViewController) -> Observable<Void> {
        viewController.rx.viewDidAppear.bind(onNext: { [unowned self] () in
             self.coordinate(to: self.loginCoordinator, from: viewController)
        }).disposed(by: disposeBag)
 
        return Observable.never()
    }
 
    func coordinate(to coordinator: CoordinatorProtocol, from viewController: UIViewController) -> Observable<Void> {
        return coordinator.start(from: viewController)
    }

Некоторые моменты требуют особо аккуратного подхода. Например, viewController.rx.viewDidAppear: этот ControlEvent не входит в стандартную библиотекую Давайте его добавим:

import Foundation
import UIKit
import RxSwift
import RxCocoa
 
extension Reactive where Base: UIViewController {
 
    private func controlEvent(for selector: Selector) -> ControlEvent<Void> {
        return ControlEvent(events: sentMessage(selector).map { _ in })
    }
 
    var viewWillAppear: ControlEvent<Void> {
        return controlEvent(for: #selector(UIViewController.viewWillAppear))
    }
 
    var viewDidAppear: ControlEvent<Void> {
        return controlEvent(for: #selector(UIViewController.viewDidAppear))
    }
 
    var viewWillDisappear: ControlEvent<Void> {
        return controlEvent(for: #selector(UIViewController.viewWillDisappear))
    }
 
    var viewDidDisappear: ControlEvent<Void> {
        return controlEvent(for: #selector(UIViewController.viewDidDisappear))
    }
 
}

В данном случае он нам понадобится, потому что презентовать viewController’ы впервые после запуска приложения возможно только после вызова описанного выше метода. Если вы попробуете это сделать сразу после запуска, то получите ошибку. Также в дальнейшем эта функция даст нам возможность при окончании показа дочернего ViewController’a имплементировать логику по демонстрации других экранов. Например, сначала нам необходимо показать tutorial, а после того, как пользователь его прошел, — основную часть приложения. В этом случае логика может выглядеть следующим образом:

func start(from viewController: UIViewController) -> Observable<Void> {
    viewController.rx.viewDidAppear.bind(onNext: { [unowned self] () in
        if tutorial.finished {
            self.coordinate(to: self.loginCoordinator, from: viewController)
        } else {
            self.coordinate(to: self.tutorialCoordinator, from: viewController)
        }
    }).disposed(by: disposeBag)

    return Observable.never()
}

Давайте переименуем наш ViewController в LoginViewController и свяжем с его координатором и ViewModel.

import Foundation
import RxSwift
import UIKit
 
class LoginCoordinator: CoordinatorProtocol {
 
    // MARK: Public Properties
 
    lazy var loginViewModel = LoginViewModel()
 
    var loginViewController: LoginViewController?
 
    // MARK: Private Properties
 
    private let disposeBag = DisposeBag()
 
    // MARK: CoordinatorProtocol Methods
 
    func start(from viewController: UIViewController) -> Observable<Void> {
        let nvc = UIStoryboard(name: LoginViewController.identifier, bundle: nil).instantiateInitialViewController() as? UINavigationController
        loginViewController = nvc?.topViewController as? LoginViewController
        loginViewController?.viewModel = loginViewModel
 
        viewController.present(nvc!, animated: true, completion: nil)
 
        loginViewModel.doneAction.drive(onNext: { [unowned self] () in
            self.loginViewController?.dismiss(animated: true, completion: nil)
        }).disposed(by: disposeBag)
 
        return Observable.never()
    }
 
    func coordinate(to coordinator: CoordinatorProtocol, from viewController: UIViewController) -> Observable<Void> {
        return coordinator.start(from: viewController)
    }
 
}

Именно в координаторе инициализируется наш LoginViewController и связывается с его ViewModel. Также не забываем, что после self.loginViewController?.dismiss(animated: true, completion: nil) будет вызван код в AppCoordinator’e.

В итоге мы получаем следующую архитектурную схему приложения:

Обратите внимание: этот подход дает возможность AppCoordinator’y самостоятельно передавать управление другим координаторам, а также организовать передачу управления между ними.

3. Q&A вопрос: Зачем нужны координаторы вообще? Я могу справиться и без них!

Ответ: Конечно, можете! Но именно координаторы дают возможность организовать простую, понятную архитектуру вашего приложения. Что, если вам необходимо после запуска приложения показать экран уведомлений или перейти в чат (после Push-Уведомления, например)? С MVVM-C вы можете прозрачно и просто передать управление соответствующему координатору. Что, если у вас есть необходимость на одном экране демонстрировать несколько View-ViewModel и организовать их взаимодействие? Координатор также выручит вас в этой ситуации! Более подробно мы остановимся на этом во второй части.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Читайте в нашем блоге

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: