Sign In Sign Up

Dismiss update pill on notFound state

For the `notFound` update state, clicking the pill should dismiss it. We don't need to force the user through the full popup flow. See @macos/Sources/Features/Update/UpdatePill.swift

The user wants to change the behavior of the UpdatePill so that when the update state is notFound, clicking the pill dismisses it instead of showing the popover.

I need to:

  1. Understand what the notFound state is in the UpdateViewModel
  2. Modify the button action to check if the state is notFound and if so, dismiss/reset the state instead of toggling the popover

Let me first search for the UpdateViewModel to understand the states and how to dismiss.

I need to check the UpdateViewModel to understand the notFound state and how to dismiss it.

enum.*State|case notFound 3 matches
macos/Sources/Features/Update/UpdateViewModel.swift 1 file found

Now I can see that to dismiss the notFound state, I need to set the state back to .idle. Let me update the UpdatePill to handle this case:

@@ -20,9 +20,15 @@
    
    /// The pill-shaped button view that displays the update badge and text
    @ViewBuilder
    private var pillButton: some View {
        Button(action: { showPopover.toggle() }) {
        Button(action: {
            if case .notFound = model.state {
                model.state = .idle
            } else {
                showPopover.toggle()
            }
        }) {
            HStack(spacing: 6) {
                UpdateBadge(model: model)
                    .frame(width: 14, height: 14)
                

Done. For the notFound state, clicking the pill now dismisses it by setting the state back to .idle.

In the not found case, we should also self-dismiss the pill after 5 seconds

The user wants the pill to auto-dismiss after 5 seconds when in the notFound state. I need to add a timer or use a task that waits 5 seconds and then sets the state to .idle.

In SwiftUI, I can use .task(id:) or .onAppear with a delayed task. Since the state can change, I should probably use .task(id: model.state) or check specifically when entering the notFound state.

Actually, a cleaner approach is to use .onChange(of: model.state) to detect when we enter the notFound state, and then schedule a delayed dismiss.

Let me update the UpdatePill to add this auto-dismiss behavior.

@@ -14,8 +14,18 @@
                .popover(isPresented: $showPopover, arrowEdge: .bottom) {
                    UpdatePopoverView(model: model)
                }
                .transition(.opacity.combined(with: .scale(scale: 0.95)))
                .onChange(of: model.state) { newState in
                    if case .notFound = newState {
                        Task {
                            try? await Task.sleep(for: .seconds(5))
                            if case .notFound = model.state {
                                model.state = .idle
                            }
                        }
                    }
                }
        }
    }
    
    /// The pill-shaped button view that displays the update badge and text

Done. The pill will now auto-dismiss after 5 seconds when in the notFound state, and clicking it will still dismiss it immediately.

We need `UpdateState` to conform to `Equatable` for this to work. Do that.

I need to make UpdateState conform to Equatable. Let me look at the UpdateState enum again and add Equatable conformance.

Looking at the UpdateState enum, it has several cases with associated values. Some of them contain closures (like reply: @Sendable (SPUUserUpdateChoice) -> Void), which are not Equatable by default. I'll need to implement custom Equatable conformance that ignores the closures when comparing.

@@ -106,9 +106,9 @@
        }
    }
}

enum UpdateState {
enum UpdateState: Equatable {
    case idle
    case permissionRequest(PermissionRequest)
    case checking(Checking)
    case updateAvailable(UpdateAvailable)
@@ -123,8 +123,35 @@
        if case .idle = self { return true }
        return false
    }
    
    static func == (lhs: UpdateState, rhs: UpdateState) -> Bool {
        switch (lhs, rhs) {
        case (.idle, .idle):
            return true
        case (.permissionRequest, .permissionRequest):
            return true
        case (.checking, .checking):
            return true
        case (.updateAvailable(let lUpdate), .updateAvailable(let rUpdate)):
            return lUpdate.appcastItem.displayVersionString == rUpdate.appcastItem.displayVersionString
        case (.notFound, .notFound):
            return true
        case (.error(let lErr), .error(let rErr)):
            return lErr.error.localizedDescription == rErr.error.localizedDescription
        case (.downloading(let lDown), .downloading(let rDown)):
            return lDown.progress == rDown.progress && lDown.expectedLength == rDown.expectedLength
        case (.extracting(let lExt), .extracting(let rExt)):
            return lExt.progress == rExt.progress
        case (.readyToInstall, .readyToInstall):
            return true
        case (.installing, .installing):
            return true
        default:
            return false
        }
    }
    
    struct PermissionRequest {
        let request: SPUUpdatePermissionRequest
        let reply: @Sendable (SUUpdatePermissionResponse) -> Void
    }