7

RxSwift で MVVM を作成しようとしていますが、Objective-C の ReactiveCocoa で行っていたことと比較すると、正しい方法でサービスを作成するのが少し難しくなっています。

例として、ログイン サービスがあります。

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");
}];

私のviewModelは次のようになります。

// 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)

と私のviewModel

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) ユーザー名とパスワードのフィールドをパラメーターとしてビューモデルに送信する必要があります (ちなみに、その方が解決しやすいです)

2)ログインボタンがタップされたときにviewControllerがすべての作業を行う方法が好きではありません。viewControllerは、ログインアクセスを取得するためにどのサービスを呼び出す必要があるかを知る必要はありません。これはviewModelの仕事です。

3 ) サブスクリプションの外部にあるユーザー名とパスワードの保存された値にアクセスできません。

これを行う別の方法はありますか?Rx'ersはこの種のことをどのようにしていますか?どうもありがとう。

4

1 に答える 1

8

Rx アプリケーションの View-Model は、入力イベント (ボタン タップ、テーブル\コレクション ビューの選択などの UI トリガーなど) のストリーム (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 を入力パラメーターとして渡します。そのため、ビジネス ロジックとプレゼンテーション ロジックは UI ロジックと密接に結合されていません。モック データを送信するときの単体テストなどで、必ずしも UI ではなく、任意のソースから View-Model 入力を提供します。つまり、ビジネス ロジックに関係なく UI を変更でき、その逆も可能です。

つまり、View-Models を使用しないimport UIKitでください。問題ありません。

2.ログインボタンがタップされたときにviewControllerがすべての作業を行う方法が好きではありません。viewControllerは、ログインアクセスを取得するためにどのサービスを呼び出す必要があるかを知る必要はありません。これはviewModelの仕事です。</p>

はい、そうです。これはビジネス ロジックであり、MVVM パターンでは、View Controller はその責任を負うべきではありません。すべてのビジネス ロジックは View-Model に実装する必要があります。そして、私の例では、このすべてのロジックが View-Model で行われ、ViewController がほとんど空であることがわかります。補足として、ViewController には多くのコード行を含めることができます。ポイントは問題の分離です。ViewController は UI ロジック (ログインが無効になっている場合の色の変更など) のみを処理する必要があり、プレゼンテーションとビジネス ロジックは View-Model によって処理されます。 .

  1. サブスクリプション以外では、保存されているユーザー名とパスワードの値にアクセスできません。

これらの値には Rx 方式でアクセスする必要があります。たとえば、View-Model がこれらの値を提供する Variable を、おそらく何らかの処理後に提供するようにします。リクエスト)。このようにして、状態と同期の問題を回避します (たとえば、保存された値を取得してラベルに表示しましたが、1 秒後に値が更新され、別のラベルが更新された値を表示しています)。

Microsoftの MVVM ダイアグラム

ここに画像の説明を入力

この情報がお役に立てば幸いです:)

関連記事:

Model-View-ViewModel for iOS By Ash Furrow http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/

RxSwift の世界のViewModel

于 2016-07-06T13:15:16.420 に答える