【问题标题】:RxSwift right wayRxSwift 正确的方式
【发布时间】:2016-06-15 15:20:24
【问题描述】:

我正在尝试使用 RxSwift 编写一个 MVVM,并且与我过去在 ReactiveCocoa for 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");
}];

而我的 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)

和我的视图模型

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'ers 是怎么做这种事情的?非常感谢。

【问题讨论】:

    标签: swift mvvm reactive-cocoa frp rx-swift


    【解决方案1】:

    我喜欢将 Rx 应用程序中的 View-Model 视为一个组件,它获取输入事件的流(Observables\Drivers)(例如 UI 触发器,如按钮点击、表格\集合视图选择等)和依赖项如 APIService、Data Base 服务等来处理这些事件。作为回报,它提供要呈现的值的流(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 和 Dependencies 看起来像这样:

    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,反之亦然。

    ​换句话说,不要在你的视图模型中import UIKit,你会没事的。

    2.我不喜欢我的 viewController 在点击登录按钮时完成所有工作的方式,viewController 不需要知道它应该调用哪个服务来获取登录访问权限,这是一个 viewModel 工作。 ​

    是的,你是对的,这是业务逻辑,在 MVVM 模式中,视图控制器不应该对此负责。所有的业务逻辑都应该在 View-Model 中实现。 您可以在我的示例中看到所有这些逻辑都发生在 View-Model 中,而 ViewController 几乎是空的。附带说明一下,ViewController 可以包含多行代码,重点是关注点分离,ViewController 应该只处理 UI 逻辑(例如,禁用登录时颜色变化),并且视图模型处理表示和业务逻辑.

    1. 我无法在订阅之外访问用户名和密码的存储值。

    您应该以 Rx 方式访问这些值。例如让 View-Model 提供一个为您提供这些值的变量,可能在一些处理之后,或者一个为您提供相关事件的驱动程序(例如,在发送登录请求之前显示一个询问“是(用户名)您的用户名吗?”的警报视图)。这样可以避免状态和同步问题(例如,我得到了存储的值并将其显示在标签上,但一秒钟后它得到更新,另一个标签显示更新后的值)

    来自Microsoft的MVVM图

    希望这些信息对您有所帮助:)

    相关文章:

    Ash Furrow http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/ 的 iOS 模型-视图-视图模型

    RxSwift 世界中的 ViewModel 由 Serg Dort https://medium.com/@SergDort/viewmodel-in-rxswift-world-13d39faa2cf5#.wuthixtp9

    【讨论】:

      猜你喜欢
      • 2016-04-12
      • 1970-01-01
      • 2016-06-24
      • 1970-01-01
      • 2018-08-19
      • 2017-09-05
      • 1970-01-01
      • 2016-09-26
      • 2019-01-19
      相关资源
      最近更新 更多