Skip to content

Instantly share code, notes, and snippets.

@marcomasser
Created April 19, 2024 13:36
Show Gist options
  • Save marcomasser/e22e73ad06a9d847eca8c494149079cd to your computer and use it in GitHub Desktop.
Save marcomasser/e22e73ad06a9d847eca8c494149079cd to your computer and use it in GitHub Desktop.
import SwiftUI
@main
struct KeyCommandsTestApp: App {
@State var counter: Counter = .init()
var body: some Scene {
WindowGroup {
ContentView(counter: self.counter)
}
.commands {
CounterCommands(counter: self.counter)
}
}
}
struct ContentView: View {
var counter: Counter
@AppStorage("UseSubMenu") var useSubMenu: Bool = true
var body: some View {
Text("""
The “Counter” main menu contains two buttons for increasing and decreasing the count. They can be triggered using the keyboard shortcuts ⌘↑ to increase and ⌘↓ to decrease. The counter can go from 0 to 3 and the buttons in the main menu as well as those next to the counter are disabled appropriately.
Steps to reproduce (assuming the app has just launched):
• Make sure the checkbox below is checked
• Press ⌘↑ three times for the counter to reach the maximum of 3
• ✅ Notice the log messages indicating that the commands were updated
• Press ⌘↑ one more time
• ✅ The app beeps
• Press ⌘↓ three times for the counter to reach the minimum of 0
• 🤨 Notice that the log messages are missing, meaning the commands were not updated
• But notice that the button next to the counter text below was disabled correctly
• Press ⌘↓ one more time
• 🐞 Instead of beeping, a console message should indicate that the action was called unexpectedly
If you uncheck the checkbox, you can see that the log messages about updating the commands appear consistently (you might need to re-launch the app to get it out of an inconsistent state).
So it seems that if these buttons are directly in the “Counter” main menu, validation works correctly. But if the buttons are placed in a submenu, validation sometimes does not work correctly.
My conclusion is that using a Button inside a Menu inside a CommandMenu breaks main menu validation ☹️
"""
).frame(width: 620.0, height: 450.0)
Toggle("Use Submenu in “Counter” main menu (necessary to reproduce the bug)", isOn: self.$useSubMenu)
.padding(.vertical)
HStack {
Button("-") {
self.counter.decrease()
}
.disabled(self.counter.canDecrease == false)
Text("\(self.counter.count)")
.fontDesign(.monospaced)
Button("+") {
self.counter.increase()
}
.disabled(self.counter.canIncrease == false)
}
.padding()
}
}
struct CounterCommands: Commands {
var counter: Counter
@AppStorage("UseSubMenu") var useSubMenu: Bool = true
var body: some Commands {
CommandMenu("Counter") {
if self.useSubMenu {
CountMenu(counter: self.counter)
} else {
makeButtonsForMenu(using: self.counter)
}
}
}
}
struct CountMenu: View {
var counter: Counter
var body: some View {
// 🐞 This menu right here seems to break the validation of the buttons inside it!
Menu("Count") {
makeButtonsForMenu(using: self.counter)
}
}
}
@ViewBuilder
func makeButtonsForMenu(using counter: Counter) -> some View {
let _ = print(#function, "was called to update the menu commands. canIncrease: \(counter.canIncrease), canDecrease: \(counter.canDecrease)")
Button("-") {
counter.decrease()
}
.disabled(counter.canDecrease == false)
.keyboardShortcut(.init(.downArrow, modifiers: .command))
Button("+") {
counter.increase()
}
.disabled(counter.canIncrease == false)
.keyboardShortcut(.init(.upArrow, modifiers: .command))
}
@Observable
final class Counter {
var count: Int = 0
var canIncrease: Bool { self.count < 3 }
var canDecrease: Bool { self.count > 0 }
func increase() {
guard self.canIncrease else {
print("🐞 \(#function) was called, but cannot increase! Button validation must have failed!")
return
}
self.count += 1
}
func decrease() {
guard self.canDecrease else {
print("🐞 \(#function) was called, but cannot decrease! Button validation must have failed!")
return
}
self.count -= 1
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment