21 ноября 2017

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

И снова здравствуйте, дорогие читатели! Мы продолжаем цикл статей, посвященных iOS-разработке; сегодня мы детально поговорим о таких вещах, как организация работы между View и ViewModel, разберем несколько примеров и создадим небольшой экран, функционал которого хотя бы раз в своей работе создавал любой iOS-разработчик.

Итак, согласно схеме ниже, которую вы могли увидеть в первой части, можно сделать вывод, что ViewModel связан со своей View всего двумя типами сигналов:

  1. Входные сигналы. Здесь все просто: любое событие, которое происходит на View вследствие воздействия на него пользователя.
  2. Выходные сигналы. Они включают команды к нашему View на его обновление. Например: изменить цвет фона, состояние кнопки и прочее.

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

Давайте рассмотрим на примере реализацию подобного правила:

Итак у нас есть простой экран логина, который состоит из:

  1. UILabel, содержащий текст «Enter your username:».
  2. UITextField для ввода username.
  3. UIBarButtonItem с заголовком Done, которая становится активна, когда пользователь вводит корректный username.

Итак, наш LoginViewController будет содержать следующий код:

import Foundation
import UIKit
import RxSwift
import RxCocoa

class LoginViewController: UIViewController, ViewControllerProtocol, Identifierable {
    
    // MARK: ViewControllerProtocol Properties
    
    typealias VM = LoginViewModel
    
    var viewModel: LoginViewModel!
    
    // MARK: Private Properties
    
    fileprivate let disposeBag = DisposeBag()
    
    // MARK: - IBOutlets: Controls
    
    @IBOutlet weak var loginTextField: UITextField!
    
    @IBOutlet weak var doneBarButtonItem: UIBarButtonItem!
    
    // MARK: Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bindUIActions()
    }
    
}

private extension LoginViewController {
    
    func bindUIActions() {
        loginTextField.rx.text.bind(to: viewModel.inputUserLogin).disposed(by: disposeBag)
        viewModel.isDoneButtonEnabled.drive(doneBarButtonItem.rx.isEnabled).disposed(by: disposeBag)
        doneBarButtonItem.rx.tap.bind(to: viewModel.doneActionObserver).disposed(by: disposeBag)
    }
    
}

Давайте рассмотрим метод bindUIActions чуть подробнее:

  1. loginTextField мы связываем с Observer’ом нашей ViewModel для того, чтобы получать уведомления при изменении текста в самом textfield.
  2. Мы связываем состояние isEnabled у нашей UIBarButtonItem с Driver’ом. Наша Кнопка будет уведомлена о необходимости изменить состояние согласно требованиям ViewModel.
  3. Связываем событие о нажатии на нашу UIBarButtonItem с ViewModel, вследствие чего при нажатии пользователем последней наш ViewModel сразу узнает об этом.

Теперь давайте рассмотрим содержание LoginViewModel:

import Foundation
import RxSwift
import RxCocoa
class LoginViewModel: ViewModelProtocol {
   // MARK: Public Properties - Inputs
   let inputUserLogin: AnyObserver<String?>
   let doneActionObserver: AnyObserver<Void>
   // MARK: Public Properties - Outputs
   let isDoneButtonEnabled: Driver<Bool>
   let doneAction: Driver<Void>
   // MARK: Private Properties
   private let disposeBag = DisposeBag()
   // MARK: Lifecycle
   init() {
       let _inputUserLogin = BehaviorSubject<String?>(value: nil)
       inputUserLogin = _inputUserLogin.asObserver()
       isDoneButtonEnabled = _inputUserLogin.asObservable().map({ $0?.count != 0 }).asDriver(onErrorJustReturn: false)
       let _doneAction = PublishSubject<Void>()
       doneAction = _doneAction.asDriver(onErrorJustReturn: ())
       doneActionObserver = _doneAction.asObserver()
       _doneAction.withLatestFrom(_inputUserLogin).bind { (login) in
           demoSDK.authService.loginObserver.onNext(login)
       }.disposed(by: disposeBag)
   }
}

Разберем чуть подробнее все, что описано в методе init():

  1. Мы создаем внутренний BehaviorSubject для хранения логина. Хочу напомнить, что BehaviorSubject отличается от PublishSubject тем, что может принимать начальное значение.
  2. Выделяем inputUserLogin для работы с нашим UITextField.
  3. Объявляем isDoneButtonEnabled, сразу привязывая логику к _inputUserLogin. Он будет уведомлять наблюдателя при каждом изменении, причем возвращать true, только когда длина строки != 0.
  4. Объявляем _doneAction, далее разделяя его на Driver и Observer. Driver нам нужен будет для уведомления наблюдателя о том, что произошло нажатие, а Observer — для того, чтобы связать событие на View.
  5. Связываем _doneAction событие с дополнительной логикой. Мы получаем последнее значение логина и при помощи специального сервиса сохраняем его.

Запускаем, смотрим, что у нас получилось:

Как мы видим, все функционирует корректно, при удалении логина кнопка вновь становится неактивной.

P.S. Насколько вы успели заметить, RxSwift позволяет с минимальными трудовыми затратами связать логику, четко ее разделить между View и ViewModel. Хочу обратить ваше внимание, что возможно создавать свои Binder’ы, если таковые не имеются в стандартной библиотеке RxCocoa. Словом, RxSwift таит в себе много интересных вещей! Всем безбажного кода на RxSwift ;)

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

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

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

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