I'm trying to write a MVVM with RxSwift and comparing to what I was used to do in ReactiveCocoa for Objective-C it's been a little hard to write my service in the right way.
An exemple is a Login service.
With ReactiveCocoa (Objective-C) I code something like this:
// 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");
}];
and my viewModel should be like this:
// 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];
}];
now in RxSwift I've written the same as:
// 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)
I'm changing the button state this way because I can't this to work:
viewModel.isCredentialsValid.drive(self.signInButton.rx_enabled).addDisposableTo(disposeBag)
and my 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 }
and
func login(username: String, password: String) -> Observable<AutenticationStatus>
{
return APIServer.sharedInstance.login(username, password: password)
}
I'm using Driver because it wrap some nice features like: catchErrorJustReturn(), but I really don't like the way I'm doing this:
1) I have to send username and password fields as a parameter to the viewModel (by the way, that's the easier to solve)
2 ) I don't like the way my viewController do all the work when login button is tapped, viewController doesn't need to know which service it should call to get login access, it's a viewModel job.
3 ) I can't access the stored value of username and password outside of a subscription.
Is there a different way to do this? how are you Rx'ers doing this kind of thing? Thanks a lot.