【问题标题】:.subscribe(onSuccess/onError) not being called.subscribe(onSuccess/onError) 没有被调用
【发布时间】:2021-10-25 06:45:46
【问题描述】:

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

我想要实现的是:

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

但是,同时使用 AppCoordinatorMVVMsubscriberobserver 似乎不起作用,因为:

  • 在来自 API 的 success 响应中,不显示成功警报。
  • 在来自 API 的 error 响应中,不显示错误警报。

我尝试过调试,但由于我对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. 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. Lo​​gInCoordinator.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. Lo​​gInViewModel.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. Lo​​ginViewController.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)
    }
}

【问题讨论】:

  • 登录服务是否调用? (您使用了 '.bind { viewModel.onLoginClicked() }' )。如果没有,请考虑使用 .bind(onNext: viewModel.onLoginClicked())
  • @MohammadReza,是的,登录服务调用,但是我没有得到回调或链接到我的视图模型/协调器方法...... (在上面的打印语句中)
  • 您能否在func login(username: String, password: String) -&gt; Single&lt;AuthResponse&gt; 的最后一行之后添加.debug("SessionService") 以及在onLoginClicked() 方法中的.disposed(by: disposeBag) 之前添加.debug("onLoginClicked")。运行应用按下按钮并将日志粘贴到此处以查看 Observable (Single) 的生命周期。

标签: ios swift mvvm rx-swift rx-cocoa


【解决方案1】:

简答

您正在制作两个不同的 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() }
        }
}

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-05
    • 1970-01-01
    • 2018-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多