2016-03-02 99 views
7

我想用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做這種事情?非常感謝。

回答

8

我喜歡將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 。

  1. 我無法在訂閱之外訪問存儲的用戶名和密碼值。

您應該以Rx方式訪問這些值。例如讓View-Model提供一個變量,在發送登錄請求之前,爲你提供這些值,可能經過一些處理,或者給你相關事件的驅動程序(例如顯示一個警告視圖,詢問「Is(userName)你的用戶名? )。這樣你可以避免狀態,和同步問題(例如我得到了存儲的值,並呈現它的標籤上,但一秒鐘後它得到了更新,另一個標籤是呈現更新後的值)從Microsoft

MVVM圖enter image description here

希望你會發現這個信息有幫助:)

相關的主題文章:

模型 - 視圖 - 視圖模型適用於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