基于RxSwift与MVVM模式的iOS应用注册登录逻辑实现问询
嘿,我来帮你梳理下怎么用RxSwift+MVVM优雅地扩展注册逻辑,同时和现有登录流程保持一致的响应式风格~
一、先对齐现有登录模块的核心逻辑
先确认下你现有LoginViewModel的loggedIn: Driver<Bool>大概是这么实现的(和GitHub示例的思路一致):
class LoginViewModel { let loggedIn: Driver<Bool> init(input: ( username: Driver<String>, password: Driver<String>, loginTap: Signal<Void> ), dependency: AuthService) { // 登录请求流:点击按钮时合并用户名密码,调用API let loginRequest = input.loginTap .withLatestFrom(Observable.combineLatest(input.username, input.password)) .flatMapLatest { username, password in dependency.login(username: username, password: password) .asDriver(onErrorJustReturn: false) } loggedIn = loginRequest } }
这种输入→转换→输出的响应式结构是规范的,注册逻辑可以完全复用这个模式,只是调整输入项和业务逻辑。
二、两种扩展注册逻辑的方案
根据你的页面结构(是单独的注册页,还是登录/注册在同一页面切换),可以选下面两种方案:
方案1:独立的RegisterViewModel(推荐,职责分离)
如果登录和注册是两个独立页面,单独做一个RegisterViewModel,和LoginViewModel平行,输出注册成功的状态流,让RegisterViewController订阅响应。
示例代码:
class RegisterViewModel { // 注册成功的信号 let registrationSuccess: Driver<Void> // 加载状态 let isLoading: Driver<Bool> // 错误提示信息 let errorMessage: Driver<String?> // 表单是否合法(控制注册按钮是否可用) let isFormValid: Driver<Bool> init(input: ( username: Driver<String>, email: Driver<String>, password: Driver<String>, confirmPassword: Driver<String>, registerTap: Signal<Void> ), dependency: AuthService) { // 1. 实时验证表单合法性 let isUsernameValid = input.username.map { $0.count >= 3 } let isEmailValid = input.email.map { $0.range(of: #"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#, options: .regularExpression) != nil } let isPasswordValid = input.password.map { $0.count >= 6 } let isPasswordMatch = Observable.combineLatest(input.password, input.confirmPassword) .map { $0 == $1 } .asDriver(onErrorJustReturn: false) isFormValid = Observable.combineLatest(isUsernameValid, isEmailValid, isPasswordValid, isPasswordMatch) .map { $0 && $1 && $2 && $3 } .asDriver(onErrorJustReturn: false) // 2. 注册请求流:点击按钮时合并表单数据,调用注册API let registerRequest = input.registerTap .withLatestFrom(Observable.combineLatest(input.username, input.email, input.password)) .flatMapLatest { username, email, password in dependency.register(username: username, email: email, password: password) .trackActivity(dependency.loadingIndicator) // 用RxSwiftExt的工具追踪加载状态 .materialize() } .asDriver(onErrorRecover: { error in return Driver.just(.error(error)) }) // 3. 转换输出流 registrationSuccess = registerRequest .filter { $0.element != nil } .map { _ in () } isLoading = dependency.loadingIndicator.asDriver() errorMessage = registerRequest .compactMap { $0.error?.localizedDescription } .asDriver(onErrorJustReturn: nil) } }
对应的RegisterViewController只需要做UI绑定和响应:
class RegisterViewController: UIViewController { @IBOutlet weak var usernameTextField: UITextField! @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var confirmPasswordTextField: UITextField! @IBOutlet weak var registerButton: UIButton! @IBOutlet weak var errorLabel: UILabel! @IBOutlet weak var loadingIndicator: UIActivityIndicatorView! private let viewModel: RegisterViewModel private let disposeBag = DisposeBag() init(viewModel: RegisterViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() bindViewModel() } private func bindViewModel() { // 输入绑定:把UI事件传给ViewModel let input = ( username: usernameTextField.rx.text.orEmpty.asDriver(), email: emailTextField.rx.text.orEmpty.asDriver(), password: passwordTextField.rx.text.orEmpty.asDriver(), confirmPassword: confirmPasswordTextField.rx.text.orEmpty.asDriver(), registerTap: registerButton.rx.tap.asSignal() ) // 输出订阅:响应ViewModel的状态变化 viewModel.isFormValid .drive(registerButton.rx.isEnabled) .disposed(by: disposeBag) viewModel.isLoading .drive(loadingIndicator.rx.isAnimating) .disposed(by: disposeBag) viewModel.errorMessage .drive(errorLabel.rx.text) .disposed(by: disposeBag) viewModel.registrationSuccess .drive(onNext: { [weak self] in // 注册成功后跳转登录页或主界面 self?.navigationController?.popViewController(animated: true) }) .disposed(by: disposeBag) } }
方案2:合并为AuthViewModel(适合同一页面切换场景)
如果登录和注册在同一个页面(比如用Tab切换表单),可以把两个逻辑合并到一个AuthViewModel里,用枚举统一输出状态:
enum AuthState { case idle case loggingIn(Bool) case registering(Bool) case success case error(String) } class AuthViewModel { let authState: Driver<AuthState> let isLoginFormValid: Driver<Bool> let isRegisterFormValid: Driver<Bool> init(input: ( // 登录输入 loginUsername: Driver<String>, loginPassword: Driver<String>, loginTap: Signal<Void>, // 注册输入 registerUsername: Driver<String>, registerEmail: Driver<String>, registerPassword: Driver<String>, registerConfirmPassword: Driver<String>, registerTap: Signal<Void> ), dependency: AuthService) { // 登录表单验证 isLoginFormValid = Observable.combineLatest(input.loginUsername, input.loginPassword) .map { $0.count >= 3 && $1.count >= 6 } .asDriver(onErrorJustReturn: false) // 注册表单验证 isRegisterFormValid = Observable.combineLatest(input.registerUsername, input.registerEmail, input.registerPassword, input.registerConfirmPassword) .map { username, email, pwd, confirmPwd in username.count >= 3 && email.range(of: #"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#, options: .regularExpression) != nil && pwd.count >=6 && pwd == confirmPwd } .asDriver(onErrorJustReturn: false) // 登录请求流 let loginFlow = input.loginTap .withLatestFrom(Observable.combineLatest(input.loginUsername, input.loginPassword)) .flatMapLatest { username, pwd in dependency.login(username: username, password: pwd) .trackActivity(dependency.loadingIndicator) .map { _ in AuthState.success } .startWith(.loggingIn(true)) .catch { error in return Observable.just(.error(error.localizedDescription)) } } // 注册请求流 let registerFlow = input.registerTap .withLatestFrom(Observable.combineLatest(input.registerUsername, input.registerEmail, input.registerPassword)) .flatMapLatest { username, email, pwd in dependency.register(username: username, email: email, password: pwd) .trackActivity(dependency.loadingIndicator) .map { _ in AuthState.success } .startWith(.registering(true)) .catch { error in return Observable.just(.error(error.localizedDescription)) } } // 合并状态流 authState = Observable.merge(loginFlow, registerFlow) .startWith(.idle) .asDriver(onErrorJustReturn: .error("Unknown error")) } }
ViewController订阅authState就可以统一处理加载、成功、错误等状态。
三、响应式编程的规范要点
- 用
Driver处理所有UI相关的流:它保证在主线程执行、共享状态、不会抛出错误(用onErrorJustReturn或catch处理错误) - 表单验证用
combineLatest实时合并多个输入流,动态更新验证状态 - 加载状态用
trackActivity这类工具统一管理,避免重复写加载逻辑 - ViewController里不要写任何业务逻辑,只做UI绑定和响应,完全依赖ViewModel的输出
- 所有订阅都要加入
disposeBag,避免内存泄漏
内容的提问来源于stack exchange,提问作者JManke
相关产品推荐
相关产品推荐

