ios - .subscribe(onSuccess/onError) 没有被调用
问题描述
我正在尝试使用 in MVVM
using创建登录页面RxSwift + AppCoordinator
。
我想要实现的是:
- Api 请求登录
- 验证登录凭据:如果
success
-> 成功警报,如果error
-> 错误警报
但是,将AppCoordinator
andMVVM
一起使用,subscriber
andobserver
似乎不起作用,因为:
- 在
success
来自 API 的响应中,不显示成功警报。 - 在
error
API 响应时,不显示错误警报。
我已经尝试过调试,但是由于我是新手RxSwift
,所以我无法弄清楚这一点,而且我不知道哪里出错了,但是,对我来说,我的代码中的流程和逻辑似乎是正确的。
任何人都可以用更好的方法帮助/指导我或帮助发现我的代码中的任何错误吗?
提前致谢。
这是我所拥有的:
- 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
}
}
- 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.?
}
}
- 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)
}
}
}
- 会话服务.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
- 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]
}
}
- 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)
}
}
- 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)
}
}
解决方案
简短的回答
您正在制作两个不同的 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() }
}
}
请注意,这两个视图模型都很容易测试,并且因为它们很小,所以它们也可以很好地重用。
推荐阅读
- java - 为什么实体对象在方法返回后会被分离?(弹簧数据JPA)
- c# - 在c#中对浮点数数组进行排序
- mysql - 在一张表上插入后,取决于数量取决于另一张表中有多少条目 - MySQL 触发器
- amazon-web-services - 使用 Terraform v 0.11.10 的 AWS ECS 容量提供程序
- node.js - Node.js、PostgreSQL 中的事务冲突、乐观并发控制和事务重试
- docker - Docker: Couldn't find copied file in container
- python - 如何在 AWS Lambda 中将位置传递给 selenium_driver.save_screenshot("test.png")?无法从 aws lambda 保存屏幕截图
- python - 返回垂直堆叠在给定对象上方的所有对象的列表?
- css - 如何使用css隐藏svg图像内的矩形部分
- java - Java:是否可以在没有反射的情况下反序列化对象