swiftui

Weird SwiftUI bug, regarding .keyboardShortcut modifier and @AppStorage


This seems like a SwiftUI bug, and I have filed a bug report with Feedback Assistant.

The issue description is in below’s code Text View’s string(there is only one Text View), please read it and try to run the code.

I have also posted another question on stackoverflow before, which seems like a SwiftUI bug too, and that may related to this issue: SwiftUI .keyboardShortcut unexpectedly captured value inside Button's action

Here is the minimal code for reproducing the issue, I run on a new mac app project, macOS version 15.1 Beta (24B5055e), Xcode Version 16.1 beta (16B5001e):

import SwiftUI

@main struct TestDotKeyboardShortcutBugApp: App {
    @Environment(\.openWindow) var openWindow
    let windowId = "ContentView"
    var body: some Scene {
        WindowGroup {
            Button("Open Window") {
                openWindow(id: windowId, value: "")
                //                openWindow(id: windowId)
            }
        }
        WindowGroup(id: windowId, for: String.self) { _ in
            //         WindowGroup(id: windowId) {
            ContentView()
        }
        .restorationBehavior(.disabled)
    }
}

let key = "Key"
struct ContentView: View {
    @AppStorage(key) var appStorageValue: Bool = false
    var userDefaultValue: Bool {
        UserDefaults.standard.bool(forKey: key)
    }
    
    var body: some View {
        ScrollViewReader { proxy in
            Text("The bug:\nafter switching the value of below Toggle one time, and then press 'a', different values will be printed on the console,\nbut if you click the 'print value' button, same values will be printed.\nHowever, comment out line 9 & 13, then uncomment line 10 & 14, the issue will be gone.")
                .padding()
            Button("print value") {
                print("appStorageValue: \(appStorageValue), userDefaultValue: \(userDefaultValue)")
            }
            .keyboardShortcut("a", modifiers: [])
            
            Toggle(isOn: $appStorageValue) {
                Text("toggle appStorageValue")
            }
        }
    }
}

Is this a SwiftUI bug? If it is, any workaround?


Solution

  • View identity is an important concept, which I don't fully understand at times. There are various articles on this, including one at: SwiftUI id.

    I sometimes think of it as refreshing the View when nothing else triggers a change. The appStorageValue changes in the Toggle, but the View already knows about this, and the side effect print does not contribute to the view.

    Example code that works for me on MacOS 15.3.

     @main struct TestDotKeyboardShortcutBugApp: App {
         @Environment(\.openWindow) var openWindow
         let windowId = "ContentView"
         
         var body: some Scene {
             WindowGroup {
                 Button("Open Window") {
                     openWindow(id: windowId, value: "")
                 }
             }
             WindowGroup(id: windowId, for: String.self) { _ in
                 ContentView()
             }
             .restorationBehavior(.disabled)
         }
     }
    
     struct ContentView: View {
         @State private var id = UUID()
         @AppStorage("Key") var appStorageValue: Bool = false
         
         var userDefaultValue: Bool {
             UserDefaults.standard.bool(forKey: "Key")
         }
         
         var body: some View {
             Group {
                 Text("The bug:\nafter switching the value of below Toggle one time, and then press 'a', different values will be printed on the console,\nbut if you click the 'print value' button, same values will be printed.\nHowever, comment out line 9 & 13, then uncomment line 10 & 14, the issue will be gone.")
                     .padding()
                 
                 Button("print value") {
                     print("appStorageValue: \(appStorageValue), userDefaultValue: \(userDefaultValue)")
                 }
                 .keyboardShortcut("a", modifiers: [])
                 
                 Toggle(isOn: $appStorageValue) {
                     Text("toggle appStorageValue")
                 }
                 .onChange(of: appStorageValue) {
                     id = UUID()
                 }
             }
             .id(id)
         }
     }