You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:31:34