RxSwift правильный путь

Я пытаюсь написать MVVM с RxSwift, и по сравнению с тем, что я делал в ReactiveCocoa для Objective-C, было немного сложно правильно написать свой сервис.

Примером является служба входа в систему.

С ReactiveCocoa (Objective-C) я кодирую примерно так:

// ViewController


// send textfield inputs to viewmodel 
RAC(self.viewModel, userNameValue) = self.fieldUser.rac_textSignal;
RAC(self.viewModel, userPassValue) = self.fieldPass.rac_textSignal;

// set button action
self.loginButton.rac_command = self.viewModel.loginCommand;

// subscribe to login signal
[[self.viewModel.loginResult deliverOnMainThread] subscribeNext:^(NSDictionary *result) {
    // implement
} error:^(NSError *error) {
    NSLog(@"error");
}];

и моя модель представления должна быть такой:

// valid user name signal
self.isValidUserName = [[RACObserve(self, userNameValue)
                         map:^id(NSString *text) {
                             return @( text.length > 4 );
                         }] distinctUntilChanged];

// valid password signal
self.isValidPassword = [[RACObserve(self, userPassValue)
                         map:^id(NSString *text) {
                             return @( text.length > 3);
                         }] distinctUntilChanged];

// merge signal from user and pass
self.isValidForm = [RACSignal combineLatest:@[self.isValidUserName, self.isValidPassword]
                                           reduce:^id(NSNumber *user, NSNumber *pass){
                                               return @( [user boolValue] && [pass boolValue]);
                                           }];


// login button command
self.loginCommand = [[RACCommand alloc] initWithEnabled:self.isValidForm
                                            signalBlock:^RACSignal *(id input) {
                                                return [self executeLoginSignal];
                                            }];

теперь в RxSwift я написал так же, как:

// ViewController

// initialize viewmodel with username and password bindings
    viewModel = LoginViewModel(withUserName: usernameTextfield.rx_text.asDriver(), password: passwordTextfield.rx_text.asDriver())

// subscribe to isCredentialsValid 'Signal' to assign button state
   viewModel.isCredentialsValid
        .driveNext { [weak self] valid in
            if let button = self?.signInButton {
                button.enabled = valid
            }
    }.addDisposableTo(disposeBag)

// signinbutton
    signInButton.rx_tap
        .withLatestFrom(viewModel.isCredentialsValid)
        .filter { $0 }
        .flatMapLatest { [unowned self] valid -> Observable<AutenticationStatus> in
            self.viewModel.login(self.usernameTextfield.text!, password: self.passwordTextfield.text!)
            .observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Default))
        }
        .observeOn(MainScheduler.instance)
        .subscribeNext {
            print($0)
        }.addDisposableTo(disposeBag)

Я меняю состояние кнопки таким образом, потому что я не могу это работать:

viewModel.isCredentialsValid.drive(self.signInButton.rx_enabled).addDisposableTo(disposeBag)

и моя модель просмотра

let isValidUser = username
    .distinctUntilChanged()
        .map { $0.characters.count > 3 }

    let isValidPass = password
    .distinctUntilChanged()
        .map { $0.characters.count > 2 }

    isCredentialsValid = Driver.combineLatest(isValidUser, isValidPass) { $0 && $1 }

и

func login(username: String, password: String) -> Observable<AutenticationStatus>
{
    return APIServer.sharedInstance.login(username, password: password)
}

Я использую Driver, потому что он включает в себя некоторые полезные функции, такие как: catchErrorJustReturn(), но мне действительно не нравится, как я это делаю:

1) Я должен отправить поля имени пользователя и пароля в качестве параметра во viewModel (кстати, это проще решить)

2 ) Мне не нравится, как мой viewController выполняет всю работу при нажатии кнопки входа в систему, viewController не нужно знать, какую службу он должен вызвать, чтобы получить доступ для входа в систему, это задание viewModel.

3) Я не могу получить доступ к сохраненному значению имени пользователя и пароля вне подписки.

Есть ли другой способ сделать это? как вы, Rx'еры, делаете такие вещи? Большое спасибо.


person Antonio Junior    schedule 02.03.2016    source источник


Ответы (1)


Мне нравится думать о View-Model в приложении Rx как о компоненте, который получает потоки (Observables\Drivers) входных событий (например, триггеры пользовательского интерфейса, такие как нажатие кнопки, выбор представления таблицы\коллекции и т. д.) и зависимости, такие как APIService , служба базы данных и т. д. для обработки этих событий. Взамен он предоставляет потоки (Observables\Drivers) значений для представления. Например:

enum ServerResponse {
  case Failure(cause: String)
  case Success
}

protocol APIServerService {
  func authenticatedLogin(username username: String, password: String) -> Observable<ServerResponse>
}

protocol ValidationService {
  func validUsername(username: String) -> Bool
  func validPassword(password: String) -> Bool
}


struct LoginViewModel {

  private let disposeBag = DisposeBag()

  let isCredentialsValid: Driver<Bool>
  let loginResponse: Driver<ServerResponse>


  init(
    dependencies:(
      APIprovider: APIServerService,
      validator: ValidationService),
    input:(
      username:Driver<String>,
      password: Driver<String>,
      loginRequest: Driver<Void>)) {


    isCredentialsValid = Driver.combineLatest(input.username, input.password) { dependencies.validator.validUsername($0) && dependencies.validator.validPassword($1) }

    let usernameAndPassword = Driver.combineLatest(input.username, input.password) { ($0, $1) }

    loginResponse = input.loginRequest.withLatestFrom(usernameAndPassword).flatMapLatest { (username, password) in

      return dependencies.APIprovider.authenticatedLogin(username: username, password: password)
        .asDriver(onErrorJustReturn: ServerResponse.Failure(cause: "Network Error"))
    }
  }
}

И теперь ваш ViewController и зависимости выглядят примерно так:

struct Validation: ValidationService {
  func validUsername(username: String) -> Bool {
    return username.characters.count > 4
  }

  func validPassword(password: String) -> Bool {
    return password.characters.count > 3
  }
}


struct APIServer: APIServerService {
  func authenticatedLogin(username username: String, password: String) -> Observable<ServerResponse> {
    return Observable.just(ServerResponse.Success)
  }
}

class LoginMVVMViewController: UIViewController {

  @IBOutlet weak var usernameTextField: UITextField!
  @IBOutlet weak var passwordTextField: UITextField!
  @IBOutlet weak var loginButton: UIButton!

  let loginRequestPublishSubject = PublishSubject<Void>()

  lazy var viewModel: LoginViewModel = {
    LoginViewModel(
      dependencies: (
        APIprovider: APIServer(),
        validator: Validation()
      ),
      input: (
        username: self.usernameTextField.rx_text.asDriver(),
        password: self.passwordTextField.rx_text.asDriver(),
        loginRequest: self.loginButton.rx_tap.asDriver()
      )
    )
  }()

  let disposeBag = DisposeBag()

  override func viewDidLoad() {
    super.viewDidLoad()

    viewModel.isCredentialsValid.drive(loginButton.rx_enabled).addDisposableTo(disposeBag)

    viewModel.loginResponse.driveNext { loginResponse in

      print(loginResponse)

    }.addDisposableTo(disposeBag)
  }
}

По вашим конкретным вопросам:

1. Я должен отправить поля имени пользователя и пароля в качестве параметра для viewModel (кстати, это проще решить)

На самом деле вы не передаете поля имени пользователя и пароля в качестве параметра модели представления, вы передаете Observables\Drivers в качестве входного параметра. Так что теперь бизнес-логика и логика представления не тесно связаны с логикой пользовательского интерфейса. Вы вводите View-Model из любого источника, не обязательно из пользовательского интерфейса, например. в модульных тестах, когда вы отправляете фиктивные данные. Это означает, что вы можете изменить свой пользовательский интерфейс, не затрагивая бизнес-логику, и наоборот.

Другими словами, не import UIKit в ваших View-Models, и все будет в порядке.

2. Мне не нравится, как мой viewController выполняет всю работу при нажатии кнопки входа в систему, viewController не нужно знать, какую службу он должен вызвать, чтобы получить доступ для входа в систему, это работа viewModel. ​

Да, вы правы, это бизнес-логика, и в паттерне MVVM вью-контроллер не должен за это отвечать. Вся бизнес-логика должна быть реализована в View-Model. И Вы можете видеть в моем примере, что вся эта логика происходит во View-Model, а ViewController почти пуст. В качестве примечания, ViewController может содержать много строк кода, суть в разделении проблем, ViewController должен обрабатывать только логику пользовательского интерфейса (например, изменение цвета при отключении входа в систему), а представление и бизнес-логика связаны с View-Model. .

  1. Я не могу получить доступ к сохраненному значению имени пользователя и пароля вне подписки.

Вы должны получить доступ к этим значениям способом Rx. например пусть View-Model предоставляет переменную, которая дает вам эти значения, возможно, после некоторой обработки, или драйвер, который дает вам соответствующие события (например, показывает представление предупреждения, которое спрашивает «Является ли (userName) ваше имя пользователя?» перед отправкой запроса на вход ). Таким образом, вы избегаете проблем с состоянием и синхронизацией (например, я получил сохраненное значение и представил его на метке, но через секунду оно было обновлено, и другая метка представляет обновленное значение)

Схема MVVM от Microsoft

введите описание изображения здесь

Надеюсь, вам будет полезна эта информация :)

Статьи по Теме:

Model-View-ViewModel для iOS Эш Ферроу http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/

ViewModel в мире RxSwift Серг Дорт https://medium.com/@SergDort/viewmodel-in-rxswift-world-13d39faa2cf5#.wuthixtp9

person Guy Kahlon    schedule 06.07.2016