Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alert dismissing itself automatically (when it shouldn't!)

I am writing a SwiftUI app using the MVVM pattern. I'm running into a situation where I am presenting an app setup screen and prompting the user for a password. To perform password validation, I have a series of Combine Publishers in my ViewModel that validate against numerous criteria. If the password validates successfully, a "Complete Registration" button is enabled in my View, and upon tapping it the account is established. If an error occurs while creating the account, an Alert is supposed to be displayed. However, I am finding in my testing that when the alert is displayed it immediately dismisses itself. Here's what my ViewModel looks like:

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
        }
    }
    
}

As you can see, I have a @State variable called error that indicates whether or not there is an error situation. My View looks something like this:

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!")
                      })
            }
    }
}

You'll notice that the Alert is displayed if error is true within my ViewModel. However, I am seeing that as soon as the Alert is displayed it is immediately dismissed, even though I have not pressed the OK button to dismiss it.

I've added some logging, including a didSet method on error, and see that at some point error is, in fact, toggled back to false. However, I am not explicitly doing this anywhere within my code, so I'm at a loss as to why it is happening. Any thoughts?

like image 584
Shadowman Avatar asked Oct 30 '25 15:10

Shadowman


1 Answers

Have you tried to create @StateObject instead of @ObservedObject.

@StateObject var viewModel: ApplicationSetupViewModel

I think the issue is that your viewModel is reloading.

like image 91
Osman Avatar answered Nov 02 '25 04:11

Osman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!