stack
stack

Reputation: 45

.subscribe(onSuccess/onError) not being called

I am trying to create a login page using in MVVM using RxSwift + AppCoordinator.

What I am trying to achieve is:

  1. Api request to login
  2. Validate the Login Credentials: If success -> Success Alert, If error -> ErrorAlert

However, using the AppCoordinator and MVVM together, the subscriber and observer seems to be NOT working, because:

I have tried debugging, but since I am quite new to RxSwift, I could not figure this out, and I do not know where I am going wrong, however, to me, the flow and logic in my code seems to be correct.

can anyone help/guide me with a better approach or help spot any errors in my code?

thanks in advance.

here is what i have:

  1. AppDelegate.swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    private var appCoordinator = AppCoordinator()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
                
        appCoordinator.start()
        return true
    }
}
  1. AppCoordinator.swift
import Foundation
import RxCocoa
import RxSwift
import Swinject


class AppCoordinator: BaseCoordinator {
    let sessionService = SessionService()
    var window = UIWindow(frame: UIScreen.main.bounds)
    
    override func start() {
        navigationController.navigationBar.isHidden = true
        window.rootViewController = navigationController
        window.makeKeyAndVisible()
        
        // TODO: here you could check if user is signed in and show appropriate screen
        let coordinator = LogInCoordinator()
        coordinator.navigationController = navigationController
        start(coordinator: coordinator)
    }
}

protocol LogInListener {
    func didLogIn()
}

extension AppCoordinator: LogInListener {
    func didLogIn() {
            print("Logged In")
            // TODO: Navigate to Dashboard or any other flow
           // However, this lines of code is NOT being called at all, and I do not see 
          // the print statement either. I dont know why.?
    }
}
  1. BaseCoordinator.swift
import Foundation
import UIKit

protocol Coordinator: AnyObject {
    var navigationController: UINavigationController { get set }
    var parentCoordinator: Coordinator? { get set }
    
    func start()
    func start(coordinator: Coordinator)
    func didFinish(coordinator: Coordinator)
}
 
class BaseCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var parentCoordinator: Coordinator?
    var navigationController = UINavigationController()
    
    func start() {
        fatalError("Start method must be implemented")
    }
    
    func start(coordinator: Coordinator) {
        childCoordinators.append(coordinator)
        coordinator.parentCoordinator = self
        coordinator.start()
    }
    
    func didFinish(coordinator: Coordinator) {
        if let index = childCoordinators.firstIndex(where: { $0 === coordinator }) {
            childCoordinators.remove(at: index)
        }
    }
}
  1. SessionService.swift
import Foundation
import RxSwift
import RxCocoa
import SwiftyJSON
import Alamofire

protocol Authentication {
    func login(username: String, password: String) -> Single<AuthResponse>
}
// MARK: - SessionService
class SessionService: Authentication {
    
    func login(username: String, password: String) -> Single<AuthResponse> {
        let formHeader: HTTPHeaders? = [
            "Content-Type": "application/x-www-form-urlencoded"
        ]
        let parameters: Parameters = [
            "username": username,
            "password": password
        ]
        let decoder = JSONDecoder()
        return Single<AuthResponse>.create { single in
            AF.request(API.auth, method: .get, parameters: parameters, headers: formHeader).responseDecodable(of: AuthResponse.self, decoder: decoder, completionHandler: { _ in
                // it returns either error or success, I got Success.
                single(.success(AuthResponse()))
            })
            return Disposables.create()
        }
    }
}

And, the Below is the LogIn Part

  1. LogInCoordinator.swift
import Foundation
import RxSwift
import RxCocoa


class LogInCoordinator: BaseCoordinator {
    private let disposeBag = DisposeBag()
    
    override func start() {
        let vc = LoginViewController.instantiate()
        
        // Coordinator initializes and injects viewModel
        let logInViewModel = LogInViewModel(authentication: SessionService())
        vc.viewModel = logInViewModel
        
        // Coordinator subscribes to events and notifies parentCoordinator
        logInViewModel.didLogIn
            .subscribe(onNext: { [weak self] in
                guard let self = self else { return }
                self.navigationController.viewControllers = []
                self.parentCoordinator?.didFinish(coordinator: self)
                (self.parentCoordinator as? LogInListener)?.didLogIn()
            })
            .disposed(by: disposeBag)
        
        navigationController.viewControllers = [vc]
    }
}
  1. LogInViewModel.swift
import Foundation
import RxSwift
import RxCocoa


class LogInViewModel {
    private let disposeBag = DisposeBag()
    private let authentication: Authentication
    
    var username: BehaviorRelay<String> = BehaviorRelay(value: "")
    
    var password: BehaviorRelay<String> = BehaviorRelay(value: "")
    let isLogInActive: Observable<Bool>
    
    // events
    let didLogIn = PublishSubject<Void>()
    let logInDidFail = PublishSubject<Error>()
    
    init(authentication: Authentication) {
        self.authentication = authentication
        self.isLogInActive = Observable.combineLatest(username, password).map { $0.0 != "" && $0.1 != "" }
    }
    
    func onLoginClicked() {
        authentication.login(username: username.value, password: password.value).map { _ in }
            .observe(on: MainScheduler.instance)
            .subscribe(onSuccess: { [weak self] _ in   // not being called
                self?.didLogIn.onNext(())
            }, onFailure: { [weak self] error in      // not being called
                self?.logInDidFail.onNext(error)
            })
            .disposed(by: disposeBag)
    }
}
  1. LoginViewController.swift
import UIKit
import RxSwift
import RxCocoa
import CocoaLumberjack


class LoginViewController: UIViewController, Storyboarded {
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!
    
    private let disposeBag = DisposeBag()
    var viewModel: LogInViewModel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.emailTextField.placeholder = "Email or Username"
        self.passwordTextField.placeholder = "Password"
        self.passwordTextField.isSecureTextEntry = true
        
        viewModel = LogInViewModel(authentication: SessionService())
        self.setUpBindings()
        
    }
    private func setUpBindings() {
        guard let viewModel = viewModel else { return }
        
        emailTextField.rx.text.orEmpty
            .bind(to: viewModel.username)
            .disposed(by: disposeBag)
        
        passwordTextField.rx.text.orEmpty
            .bind(to: viewModel.password)
            .disposed(by: disposeBag)
        
        
        loginButton.rx.tap
            .bind { viewModel.onLoginClicked() }
            .disposed(by: disposeBag)
        
        viewModel.isLogInActive
            .bind(to: loginButton.rx.isEnabled)
            .disposed(by: disposeBag)
        
        viewModel.logInDidFail
            .subscribe(onNext: { error in
                print("Failed: \(error)") // not printing the line
            })
            .disposed(by: disposeBag)
    }
}

Upvotes: 1

Views: 266

Answers (1)

Daniel T.
Daniel T.

Reputation: 33979

Short Answer

You are making two different LoginViewModels. You are subscribing to the didLogIn of one of them, but sending the next event into the didLogIn of the other.

Longer Answer

This is the result of having all that unnecessary boilerplate surrounding what should be a very simple process.

Your setUpBindings() method should look more like this:

    private func setUpBindings() {
        let didFail = PublishSubject<Error>()
        let didLogin = didLogIn(
            trigger: loginButton.rx.tap.asObservable(),
            username: emailTextField.rx.text.asObservable(),
            password: passwordTextField.rx.text.asObservable(),
            login: login(username:password:),
            didFail: didFail.asObserver()
        )

        buttonEnabled(fields: [
            emailTextField.rx.text.asObservable(),
            passwordTextField.rx.text.asObservable()
        ])
        .bind(to: loginButton.rx.isEnabled)
        .disposed(by: disposeBag)

        didLogin
            .subscribe(onNext: { _ in
                print("Logged In")
                // TODO: Navigate to Dashboard or any other flow
            })
            .disposed(by: disposeBag)

        didFail
            .subscribe(onNext: { error in
                print("Failed: \(error)") // not printing the line
            })
            .disposed(by: disposeBag)
    }

Notice that there are two view models here with separate concerns. Both of them are simple functions:

func buttonEnabled(fields: [Observable<String?>]) -> Observable<Bool> {
    Observable.combineLatest(fields.map { $0.compactMap { $0 } }).map { $0.allSatisfy { !$0.isEmpty }}
}

func didLogIn(trigger: Observable<Void>, username: Observable<String?>, password: Observable<String?>, login: @escaping (String, String) -> Single<AuthResponse>, didFail: AnyObserver<Error>) -> Observable<AuthResponse> {
    let credentials = Observable.combineLatest(username.compactMap { $0 }, password.compactMap { $0 }) { (username: $0, password: $1) }
    return trigger
        .withLatestFrom(credentials)
        .flatMapLatest {
            login($0.username, $0.password)
                .asObservable()
                .catch { didFail.onNext($0); return Observable.empty() }
        }
}

Note that both of these view models are easily testable and because they are small, they are also nicely reusable.

Upvotes: 0

Related Questions