首页 > 解决方案 > .subscribe(onSuccess/onError) 没有被调用

问题描述

我正在尝试使用 in MVVMusing创建登录页面RxSwift + AppCoordinator

我想要实现的是:

  1. Api 请求登录
  2. 验证登录凭据:如果success-> 成功警报,如果error-> 错误警报

但是,将AppCoordinatorandMVVM一起使用,subscriberandobserver似乎不起作用,因为:

我已经尝试过调试,但是由于我是新手RxSwift,所以我无法弄清楚这一点,而且我不知道哪里出错了,但是,对我来说,我的代码中的流程和逻辑似乎是正确的。

任何人都可以用更好的方法帮助/指导我或帮助发现我的代码中的任何错误吗?

提前致谢。

这是我所拥有的:

  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. 会话服务.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)
    }
}

标签: iosswiftmvvmrx-swiftrx-cocoa

解决方案


简短的回答

您正在制作两个不同的 LoginViewModel。您正在订阅didLogIn其中一个,但将下一个事件发送到didLogIn另一个。

更长的答案

这是在应该是一个非常简单的过程周围有所有不必要的样板的结果。

您的 setUpBindings() 方法应该看起来更像这样:

    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)
    }

请注意,这里有两个具有不同关注点的视图模型。它们都是简单的函数:

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() }
        }
}

请注意,这两个视图模型都很容易测试,并且因为它们很小,所以它们也可以很好地重用。


推荐阅读