点击按钮时校验文本的登录逻辑实现疑问(附RxSwift代码)
Hey there! Let’s break down your questions step by step, starting with clarifying the two validation approaches you mentioned, then fixing your ViewModel code to meet your needs.
First: Why Two Different Validation Styles?
You noticed two common login Demo patterns—let’s clarify when to use each:
- Controlling button enabled state: This is real-time validation. As the user types, you check inputs immediately and only enable the button when all rules are met. It’s great for preventing invalid clicks upfront and giving instant feedback (like highlighting a wrong phone number as soon as the user stops typing).
- Validating on button tap: This lets users click the button anytime, but runs checks after the tap. It’s perfect if you want to show a unified error message (e.g., a single UILabel saying "Phone number is invalid" or "Password is too short") instead of scattered real-time hints. Both approaches are valid—pick the one that fits your app’s UX needs.
Question 1: Validate on Tap & Trigger Network Request
To implement click-time validation, we’ll add a way to pass error messages to your View, then combine the username/password validation results when the button is tapped. Here’s how to adjust your ViewModel:
- Add relays to carry error messages and login status to the View.
- Combine the latest validation results and input values when the button is tapped.
- Run checks: if any fail, send an error message; if all pass, trigger the network request.
Question 2: Handle Network Requests Returning Bool Instead of Token
This is straightforward—just adjust the request’s return type, then handle the boolean result in your subscription. You’ll no longer need to store a Token, but instead react directly to whether the login succeeded or failed.
Full Modified ViewModel Code
Here’s the updated code with comments explaining key changes:
class LoginViewModel: BaseViewModel, ViewModelType { struct Input { let loginTaps: Driver<Void> } struct Output { let validatedUsername: Driver<Bool> let validatedPassword: Driver<Bool> let errorMessage: Driver<String?> // For showing error text in UILabel let loginSuccess: Driver<Bool> // For notifying View if login worked (Bool return) } // Input value relays let username = BehaviorRelay(value: "") let password = BehaviorRelay(value: "") // Private relays for internal state management private let errorMessageRelay = BehaviorRelay<String?>(value: nil) private let loginSuccessRelay = BehaviorRelay(value: false) func transform(input: Input) -> Output { // 1. Individual field validation logic let validatedUsername = username.asDriver(onErrorJustReturn: "") .map { $0.isPhoneNumber } let validatedPassword = password.asDriver(onErrorJustReturn: "") .map { $0.count > 7 } // 2. Handle login button taps input.loginTaps // Get latest validation status and input values .withLatestFrom(Driver.combineLatest(validatedUsername, validatedPassword, username, password)) .drive(onNext: { [weak self] (isUsernameValid, isPasswordValid, currentUsername, currentPassword) in guard let self = self else { return } // Check validation results if !isUsernameValid { self.errorMessageRelay.accept("Please enter a valid phone number") return } if !isPasswordValid { self.errorMessageRelay.accept("Password must be at least 8 characters long") return } // All checks passed: clear error and trigger login self.errorMessageRelay.accept(nil) self.performLogin(username: currentUsername, password: currentPassword) }) .disposed(by: rx.disposeBag) return Output( validatedUsername: validatedUsername, validatedPassword: validatedPassword, errorMessage: errorMessageRelay.asDriver(), loginSuccess: loginSuccessRelay.asDriver() ) } // Isolated login request logic for readability private func performLogin(username: String, password: String) { // Replace with your actual network request that returns Bool provider.loginReturningBool(username: username, password: password) .asObservable() .materialize() .subscribe(onNext: { [weak self] event in guard let self = self else { return } switch event { case .next(let isSuccess): self.loginSuccessRelay.accept(isSuccess) if isSuccess { // Add your login success logic here (e.g., navigate to home) print("Login succeeded!") } else { self.errorMessageRelay.accept("Incorrect username or password") } case .error(let error): log.error(error.localizedDescription) self.errorMessageRelay.accept("Network error. Please try again later.") default: break } }) .disposed(by: rx.disposeBag) } }
How to Bind to Your View
In your ViewController, bind the ViewModel’s outputs to your UI elements like this:
// Bind error message to your UILabel viewModel.output.errorMessage .drive(errorLabel.rx.text) .disposed(by: disposeBag) // React to login success viewModel.output.loginSuccess .filter { $0 } .drive(onNext: { _ in // Navigate to your home screen here }) .disposed(by: disposeBag)
内容的提问来源于stack exchange,提问作者Ivan.Ye

