【问题标题】:SwiftUI: Alert dismissing itself automatically (when it shouldn't!)SwiftUI:警报自动关闭(当它不应该时!)
【发布时间】:2021-09-23 15:19:49
【问题描述】:

我正在使用 MVVM 模式编写一个 SwiftUI 应用程序。我遇到了一种情况,我正在呈现应用程序设置屏幕并提示用户输入密码。为了执行密码验证,我在我的ViewModel 中有一系列组合Publishers,可以根据许多标准进行验证。如果密码验证成功,则在我的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
        }
    }
    
}

如您所见,我有一个名为error@State 变量,它指示是否存在错误情况。我的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!")
                      })
            }
    }
}

如果我的ViewModel 中的错误为真,您会注意到会显示Alert。但是,我看到Alert 显示后它会立即被关闭,即使我没有按下 OK 按钮来关闭它。

我添加了一些日志记录,包括error 上的didSet 方法,并看到在某些时候错误实际上被切换回false。但是,我没有在我的代码中的任何地方明确地这样做,所以我不知道它为什么会发生。有什么想法吗?

【问题讨论】:

    标签: ios swift swiftui combine


    【解决方案1】:

    您是否尝试过创建@StateObject 而不是@ObservedObject。

    @StateObjec var viewModel: ApplicationSetupViewModel
    

    我认为问题在于您的 viewModel 正在重新加载。

    【讨论】:

    • 就是这样!那么,为了我自己的利益,这两个属性包装器有什么区别? @ObservedObject 会自动重置我的视图模型吗?
    • 不错不错。你能接受我的回答吗?您的 ObservedObject 再次被初始化。请查看链接以查找详细信息donnywals.com/…
    • 该链接还让您了解初始代码的问题所在。看起来 viewModel 是作为初始化程序的一部分传入的。在这种情况下,原始代码应该可以工作。您可能会看到这种行为,因为在您创建视图模型并将其传递给视图时,您并没有保持对视图模型的强引用。由于此视图没有创建视图模型,因此不需要使用@StateObject
    猜你喜欢
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-30
    • 1970-01-01
    • 2017-10-03
    • 2014-12-12
    • 1970-01-01
    相关资源
    最近更新 更多