我正在使用 MVVM 模式编写一个 SwiftUI 应用程序。我遇到了一种情况,我正在显示应用程序设置屏幕并提示用户输入密码。为了执行密码验证,我的
Publisher
中有一系列组合 ViewModel
,可以根据多种标准进行验证。如果密码验证成功,我的 View
中会启用“完成注册”按钮,点击该按钮即可建立帐户。如果创建帐户时发生错误,则会显示 Alert
。然而,我在测试中发现,当显示警报时,它会立即自行消失。这是我的 ViewModel
的样子:
struct ApplicationSetupState {
}
enum ApplicationSetupInput {
case completeSetup
}
class ApplicationSetupViewModel: ViewModel {
typealias ViewModelState = ApplicationSetupState
typealias ViewModelInput = ApplicationSetupInput
enum PasswordValidation {
case valid
case empty
case noMatch
}
@Published var password: String = ""
@Published var confirmPassword: String = ""
@Published var useBiometrics: Bool = false
@Published var isValid: Bool = false
@Published var state: ApplicationSetupState
@Published var errorMessage: String? = nil
@Published var error: Bool = false
private var cancellables: [AnyCancellable] = []
private var isPasswordEmptyPublisher: AnyPublisher<Bool, Never> {
$password
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { password in
DDLogVerbose("Mapping password=\(password)")
return password == ""
}
.eraseToAnyPublisher()
}
private var arePasswordsEqualPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest($password, $confirmPassword)
.debounce(for: 0.1, scheduler: RunLoop.main)
.map { password, confirmPassword in
DDLogVerbose("Mapping password=\(password), confirmPassword=\(confirmPassword)")
return password == confirmPassword
}
.eraseToAnyPublisher()
}
private var isPasswordValidPublisher: AnyPublisher<PasswordValidation, Never> {
Publishers.CombineLatest(isPasswordEmptyPublisher, arePasswordsEqualPublisher)
.map { isPasswordEmpty, arePasswordsEqual in
DDLogVerbose("Mapping isPasswordEmpty=\(isPasswordEmpty), arePasswordsEqual=\(arePasswordsEqual)")
if isPasswordEmpty {
return .empty
} else if !arePasswordsEqual {
return .noMatch
}
return .valid
}
.eraseToAnyPublisher()
}
private var isSetupValidPublisher: AnyPublisher<Bool, Never> {
isPasswordValidPublisher
.map { isPasswordValid in
return isPasswordValid == .valid
}
.eraseToAnyPublisher()
}
init() {
self.state = ApplicationSetupState()
self.isPasswordValidPublisher
.receive(on: RunLoop.main)
.map { passwordValidation in
DDLogVerbose("Mapping passwordValidation=\(passwordValidation)")
switch passwordValidation {
case .empty:
return "You must provide a master password"
case .noMatch:
return "The passwords do not match. Try again."
default:
return ""
}
}
.assign(to: \.errorMessage, on: self)
.store(in: &cancellables)
self.isSetupValidPublisher
.receive(on: RunLoop.main)
.assign(to: \.isValid, on: self)
.store(in: &cancellables)
}
func trigger(_ input: ApplicationSetupInput) {
switch input {
case .completeSetup:
self.completeApplicationSetup()
}
}
// MARK: - Private methods
private func completeApplicationSetup() {
DDLogInfo("Completing application setup...")
do {
let status = try ApplicationSetupController.shared.completeApplicationSetup(withPassword: self.password,
useBiometrics: self.useBiometrics)
DDLogVerbose("status=\(status)")
if status == true {
DDLogInfo("Application setup completed successfully. Posting notification...")
NotificationCenter.default.post(name: .didCompleteSetup, object: nil)
} else {
DDLogWarn("Application setup failed with no error message")
self.error = true
self.errorMessage = "An unknown error occurred during setup. Please try again."
}
} catch {
DDLogError("ERROR while completing application setup - \(error)")
self.error = true
self.errorMessage = error.localizedDescription
}
}
}
如您所见,我有一个名为
@State
的 error
变量,用于指示是否存在错误情况。我的View
看起来像这样:
struct MasterPasswordSetupView: View {
@ObservedObject var viewModel: ApplicationSetupViewModel
var body: some View {
VStack(alignment: .center) {
...stuff...
}
.alert(isPresented: self.$viewModel.error) {
Alert(title: Text("Error"),
message: Text(self.viewModel.errorMessage ?? ""),
dismissButton: .default(Text("Ok")) {
DDLogVerbose("OK pressed!")
})
}
}
}
您会注意到,如果我的
Alert
中的错误为真,则会显示 ViewModel
。但是,我发现一旦显示 Alert
,它就会立即被忽略,即使我没有按下“确定”按钮来关闭它。
我添加了一些日志记录,包括
didSet
上的 error
方法,并且看到在某些时候错误实际上被切换回 false。但是,我没有在代码中的任何地方明确执行此操作,因此我不知道为什么会发生这种情况。有什么想法吗?
您是否尝试过创建@StateObject而不是@ObservedObject。
@StateObject var viewModel: ApplicationSetupViewModel
我认为问题是你的 viewModel 正在重新加载。