【发布时间】:2021-10-25 06:45:46
【问题描述】:
我正在尝试使用MVVM 使用RxSwift + AppCoordinator 创建登录页面。
我想要实现的是:
- API 请求登录
- 验证登录凭据:如果
success-> 成功警报,如果error-> 错误警报
但是,同时使用 AppCoordinator 和 MVVM,subscriber 和 observer 似乎不起作用,因为:
- 在来自 API 的
success响应中,不显示成功警报。 - 在来自 API 的
error响应中,不显示错误警报。
我尝试过调试,但由于我对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)
}
}
}
- 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
- 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)
}
}
【问题讨论】:
-
登录服务是否调用? (您使用了 '.bind { viewModel.onLoginClicked() }' )。如果没有,请考虑使用 .bind(onNext: viewModel.onLoginClicked())
-
@MohammadReza,是的,登录服务调用,但是我没有得到回调或链接到我的视图模型/协调器方法...... (在上面的打印语句中)
-
您能否在
func login(username: String, password: String) -> Single<AuthResponse>的最后一行之后添加.debug("SessionService")以及在onLoginClicked()方法中的.disposed(by: disposeBag)之前添加.debug("onLoginClicked")。运行应用按下按钮并将日志粘贴到此处以查看 Observable (Single) 的生命周期。
标签: ios swift mvvm rx-swift rx-cocoa