首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >RxSwift正确之路

RxSwift正确之路
EN

Stack Overflow用户
提问于 2016-03-02 16:26:59
回答 1查看 3.2K关注 0票数 7

我正在尝试用RxSwift编写一个MVVM,并与我在ReactiveCocoa中为Objective所做的工作进行比较,用正确的方式编写我的服务有点困难。

一个例子是登录服务。

使用ReactiveCocoa (Objective),我编写了如下代码:

代码语言:javascript
运行
复制
// 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应该是这样的:

代码语言:javascript
运行
复制
// 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中,我编写了与以下内容相同的内容:

代码语言:javascript
运行
复制
// 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)

我是这样改变按钮状态的,因为我不能这样做:

代码语言:javascript
运行
复制
viewModel.isCredentialsValid.drive(self.signInButton.rx_enabled).addDisposableTo(disposeBag)

还有我的viewModel

代码语言:javascript
运行
复制
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 }

代码语言:javascript
运行
复制
func login(username: String, password: String) -> Observable<AutenticationStatus>
{
    return APIServer.sharedInstance.login(username, password: password)
}

我之所以使用驱动程序,是因为它封装了一些很好的特性,比如: catchErrorJustReturn(),但是我真的不喜欢这样做:

1)我必须将用户名和密码字段作为参数发送到viewModel (顺便说一句,这更容易解决)

2)我不喜欢我的viewController在点击登录按钮时做所有工作的方式,viewController不需要知道它应该调用哪个服务才能获得登录访问,这是一个viewModel作业。

3)无法访问订阅之外的用户名和密码的存储值。

有什么不同的方法吗?你是怎么做这种事的?非常感谢。

EN

回答 1

Stack Overflow用户

发布于 2016-07-06 13:15:16

我喜欢将Rx应用程序中的View-Model作为一个组件来考虑,它获取输入事件的流(可观察到的\驱动程序)(例如UI触发器,例如按钮点击、表\集合视图选择等),以及处理这些事件的依赖关系(如APIService、数据库服务等)。作为回报,它提供要显示的值的流(可观察的\驱动程序)。例如,​:

代码语言:javascript
运行
复制
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和依赖项看起来如下所示:

代码语言:javascript
运行
复制
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 (顺便说一句,这更容易解决)

因此,您不将用户名和密码字段作为参数传递给视图模型,而是将可观察的\驱动程序作为输入参数传递。因此,现在业务和表示逻辑并没有紧密地耦合到UI逻辑。您提供来自任何源的视图模型输入,而不一定是UI,例如在发送模拟数据时进行单元测试。这意味着您可以在不涉及业务逻辑的情况下更改UI,反之亦然。

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

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

是的,您是对的,这是业务逻辑,在MVVM模式中,视图控制器不应该对此负责。所有的业务逻辑都应该在视图模型中实现。在我的示例中可以看到,所有这些逻辑都发生在视图模型中,而ViewController几乎是空的。顺便提一句,ViewController可以包含许多代码行,重点是分离关注点,ViewController应该只处理UI逻辑(例如,当登录被禁用时改变颜色),而表示和业务逻辑则由视图模型提供。

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

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

来自微软的MVVM图

希望你会发现这个信息有帮助:)

相关条款:

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

“ViewModel in RxSwift world”,Serg Dort https://medium.com/@SergDort/viewmodel-in-rxswift-world-13d39faa2cf5#.wuthixtp9

票数 8
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/35752866

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档