RxSwift正确的方法
我想用RxSwift编写一个MVVM,并比较我在Objective-C中使用的ReactiveCocoa中的内容,但以正确的方式编写我的服务有点困难。RxSwift正确的方法
一个例子是一个登录服务。
随着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)
和我的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)
}
我使用的驱动程序,因为它换了一些不错的功能,如:catchErrorJustReturn(),但我真的不喜欢我正在这样做:
1)我必须发送用户名和密码字段作为参数到viewModel(顺便说一下,的更容易解决)
2)我不喜欢我的viewController完成所有的工作,当登录按钮被点击时,viewController不需要知道它应该调用哪个服务来获得登录访问,它是一个viewModel作业。
3)我无法访问订阅以外的用户名和密码的存储值。
有没有不同的方法来做到这一点?你好吗?Rx'ers做这种事情?非常感谢。
我喜欢将Rx应用程序中的视图模型作为获取输入事件(例如按钮水龙头,表格集合视图选择等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.I需要发送的用户名和密码字段作为viewModel的一个参数(顺便说一句,这是更容易解决的)
你很勉强不要将用户名和密码字段作为参数传递给视图模型,则将Observables \ Drivers作为输入参数传递。所以现在业务和表示逻辑并没有紧密结合到UI逻辑上。您可以从任何来源提供视图模型输入,而不一定是UI。在发送模拟数据时进行单元测试。这意味着您可以更改您的用户界面,而无需考虑业务逻辑,反之亦然。
换句话说,不要在您的视图模型import UIKit
,你会没事的。
2.我不喜欢我的viewController在登录按钮被点击时执行所有工作的方式,viewController不需要知道它应该调用哪个服务来获得登录访问权限,这是一个viewModel作业。
是的,你是对的,这是商业逻辑,而在MVVM模式中,视图控制器不应该对此负责。所有的业务逻辑都应该在视图模型中实现。 您可以在我的示例中看到,所有这些逻辑都在View-Model中进行,并且ViewController几乎为空。作为一个方面说明,ViewController可以包含许多代码行,关键点在于关注,ViewController应该只处理UI逻辑(例如,当登录被禁用时的颜色变化),并且演示文稿和业务逻辑受到View-Model 。
- 我无法在订阅之外访问存储的用户名和密码值。
您应该以Rx方式访问这些值。例如让View-Model提供一个变量,在发送登录请求之前,为你提供这些值,可能经过一些处理,或者给你相关事件的驱动程序(例如显示一个警告视图,询问“Is(userName)你的用户名? )。这样你可以避免状态,和同步问题(例如我得到了存储的值,并呈现它的标签上,但一秒钟后它得到了更新,另一个标签是呈现更新后的值)从Microsoft
希望你会发现这个信息有帮助:)
相关的主题文章:
模型 - 视图 - 视图模型适用于iOS通过灰沟http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/
ViewModel in RxSwift world按Serg Dort https://medium.com/@SergDort/viewmodel-in-rxswift-world-13d39faa2cf5#.wuthixtp9