iosswiftmvvmreactivexstarscream

Chaining login operations with RxSwift


I'm creating an app that has a specific two-way authentication process: First, a REST based login with credentials, which returns a websocket endpoint on the server, plus an authtoken to use in order to connect to it.

Only after the websocket has successfully connected to the server, an I supposed to switch to the next view.

Trying to implement under MVVM, I have created an API struct for the server net calls:

My LoginViewController binds the username and password to the LoginViewModel, which in return binds a login Action to the login button:

func onLogin() -> CocoaAction {
    return CocoaAction { _ in
        self.loginService.login(username: self.usernameText.value, password: self.passwordText.value)
        let MainViewModel = MainViewModel(sceneCoordinator: self.sceneCoordinator)
        return self.sceneCoordinator.transition(to: Scene.mainView(mainViewModel), type: .modal).asObservable().map { _ in }
    }
}

The LoginService should return a Completable for the login, in order to indicate a successful login (and move the view to the main app screen) or an error to show the user.

protocol LoginServiceType {

@discardableResult
func login(username: String, password: String) -> Completable

}

I'm having an issue with the implementation of this function. It should first call the REST login API, and after getting the response, start the connection to the websocket. The implementation for the server API is as follows (under recommended RxSwift MVVM examples):

struct Server: ServerProtocol {

// MARK: - API Errors
enum Errors: Error {
    case requestFailed
}

// MARK: - API Endpoint requests
static func login(for username: String, password: String) -> Observable<JSONObject> {
    let parameters = [
        "username": username,
        "password": password
    ]
    return request(address: Server.Address.login, parameters: parameters)
}

// MARK: - generic request to API
static private func request<T: Any>(address: Address, parameters: [String: String] = [:]) -> Observable<T> {
    return Observable.create { observer in
        let request = Alamofire.request(address.url.absoluteString,
                                        method: .post,
                                        parameters: parameters,
                                        encoding: JSONEncoding.default,
                                        headers: nil)
        request.responseJSON { response in
            guard response.result.isSuccess == true, let data = response.data,
                let json = try? JSONSerialization.jsonObject(with: data, options: []) as? T, let result = json else {
                    observer.onError(Errors.requestFailed)
                    return
            }
            observer.onNext(result)
            observer.onCompleted()
        }
        return Disposables.create {
            request.cancel()
        }
    }
}

}

So I'm trying to figure out how to connect the REST call, its response with the need endpoint+Token, to creating the Websocket and subscribing to it's connect callback, which then should return the Completable back to the LoginViewModel.

Any advice would be welcome.


Solution

  • I think "flatmap" is what you're looking for.

    Have a look:

    var mynum = Variable(0)
    let disposeBag = DisposeBag()
    
    func login() -> Observable<(String,String)> {
    
        return Observable.create { observer in
    
            // Place your server access code
    
            if (<some_condition>) { // if error
                observer.on(.error(<custome error>))
            }
    
            observer.on(.next(("websocket", "authtoken")))
            observer.on(.completed)
            return Disposables.create()
        }
    }
    
    func API(webSiteData: (String, String)) -> Observable<Int> {
    
        return Observable.create { observer in
    
            // Place your second server access code
    
            print (webSiteData.0)
            print (webSiteData.1)
    
            observer.on(.next(1)) // assiging "1" just as an example, you may ignore
            observer.on(.completed)
            return Disposables.create()
        }
    }
    
    func combine() {
        self.login().catchError({ (currentError) -> Observable<(String, String)> in
            print (currentError.localizedDescription)
            return Observable.just(("",""))
        })
            .flatMap(self.API)
            .bind(to: mynum)
            .disposed(by: self.disposeBag)
    }