1

I'm building an iOS app in SwiftUI and hoping to run it on the Mac using Catalyst as well. I've added a keyboard shortcut CMD+N using UIKeyCommand on the main hosting controller, which fires a Notification that my ObservableObject model listens to, toggling an alert property to show an alert. Here's the code:

class KeyCommandHostingController<Content: View>: UIHostingController<Content> {
    override var keyCommands: [UIKeyCommand]? {
        [
            UIKeyCommand(title: "New Game", action: #selector(postNewGame), input: "n", modifierFlags: .command)
        ]
    }

    @objc func postNewGame() {
        print("postNewGame called")
        NotificationCenter.default.post(name: .newGameRequested, object: nil)
    }

}

public extension Notification.Name {
   static let newGameRequested = Notification.Name(rawValue: "newGameRequested")
}

class Model: ObservableObject {
    @Published var presentAlert = false
    var cancellables = Set<AnyCancellable>()

    init() {
        NotificationCenter.default
            .publisher(for: .newGameRequested)
            .sink { [unowned self] _ in
                self.presentAlert.toggle()
                print("new game requested")
            }
            .store(in: &cancellables)
    }
}

struct ContentView: View {
    @ObservedObject var model: Model

    var body: some View {
        Text("Hello, World!")
            .alert(isPresented: $model.presentAlert) {
                Alert(title: Text("New Game"),
                      message: Text("Are you sure you want to start a new game? The current game will be recorded as a loss."),
                      primaryButton: Alert.Button.destructive(Text("New Game")) {
                        print("New Game selected")
                      },
                      secondaryButton: Alert.Button.cancel()
                )
            }
    }
}

When I execute the keyboard command in iPhone or iPad simulator, this behaves correctly - one notification is fired, and the alert is shown. However, in the Mac Catalyst app, hitting the keyboard shortcut once causes the notification to be endlessly fired, which causes the app to attempt to endlessly show the alert over and over. The console output confirms it, with the following infinite output (snipped):

new game requested
postNewGame called
new game requested
postNewGame called
new game requested
postNewGame called
new game requested
2020-03-29 14:04:11.179475-0400 AlertMadness[1332:29452] Warning: Attempt to present <SwiftUI.PlatformAlertController: 0x10192de00> on <_TtGC12AlertMadness27KeyCommandHostingControllerVS_11ContentView_: 0x10310bcf0> while a presentation is in progress!
postNewGame called
new game requested
postNewGame called
new game requested
postNewGame called
new game requested
postNewGame called
new game requested
2020-03-29 14:04:11.595161-0400 AlertMadness[1332:29452] Warning: Attempt to present <SwiftUI.PlatformAlertController: 0x10195ca00> on <_TtGC12AlertMadness27KeyCommandHostingControllerVS_11ContentView_: 0x10310bcf0> while a presentation is in progress!
(etc...)

Am I doing something wrong, or is this a bug in Catalyst/SwiftUI? Using Xcode 11.4 and macOS 10.15.4.

UberJason
  • 3,063
  • 2
  • 25
  • 50
  • I'm having a similar issue. I didn't add any code for handling keyboard input but I added multi-window support and my catalyst app just keeps creating new windows after hitting Command-n? Also xcode 11.4 and 10.15.4 macOS – Joel Klabo Apr 14 '20 at 14:31
  • For what it's worth, I found later that putting the UIKeyCommand in the AppDelegate's `buildMenu(with:)` method as well as in the UIHostingController's `keyCommands` property prevents the infinite trigger from occurring. It's a little duplicative, but it helped me. – UberJason Apr 15 '20 at 00:16
  • Thanks, I will try that. Glad to hear I'm not the only one experiencing this at least. – Joel Klabo Apr 15 '20 at 15:53
  • I ran into the same problem, never finding the cause, and kept calling methods -.- – iHTCboy Apr 19 '20 at 01:37
  • @UberJason Any updates since? I've submitted a Feedback. – George Apr 26 '20 at 17:55
  • Only what I mentioned earlier, that putting the UIKeyCommand in the AppDelegate’s `buildMenu(with:)` method as well helps prevent the issue. I didn't love the duplication (I essentially copy-pasted the UIKeyCommand definition from the hosting controller), but it does resolve for me. – UberJason Apr 26 '20 at 21:48
  • @UberJason Thanks for the reply! Can you help me a little bit more there? Did you duplicate `override var keyCommands: [UIKeyCommand]?`, or something else (because I obviously can't return a `[UIKeyCommand]` within `buildMenu(with:)`)? I can't seem to figure it out – George Apr 26 '20 at 22:01
  • @George_E Since I was building a Catalyst app, I also wanted my key commands to be available from a Main Menu for the Mac following the example in this sample code: https://developer.apple.com/documentation/uikit/uicommand/adding_menus_and_shortcuts_to_the_menu_bar_and_user_interface When I added those key commands to a top-level menu in the menu bar, it had the bonus side effect that this infinite repeating behavior disappeared in my view controller. It was a win-win for me. Here's also some documentation on UIMenuBuilder: https://developer.apple.com/documentation/uikit/uimenubuilder – UberJason Apr 27 '20 at 18:43
  • Obviously adding an entire Mac Menu *just* to fix your view controller issue is overkill and possibly not what you want. But hopefully you're considering adding menus in your Catalyst app as well, in which case this would help. – UberJason Apr 27 '20 at 18:44
  • I submitted feedback as well. FB7644774, feel free to dupe. Project here: https://github.com/UberJason/CatalystSwiftUIAlertBugs – UberJason Apr 27 '20 at 18:48
  • @UberJason I found [this](https://stackoverflow.com/a/57451524/9607863) works well enough, but it still has its flaws. – George Apr 27 '20 at 19:07

1 Answers1

0

I was having a similar issue with creating new windows. I hadn't created any custom actions but my iPad app had multi-window support so Command-N just "worked" when I built the mac app.

The only issue was that it wouldn't stop creating new windows. Eventually I found out that in my migration from Swift 4 -> 5 I had ended up with an incorrect UIApplicationDelegate callback. So, appDidFinishLaunching was never called.

I then used autocomplete to get the right method signature and that fixed my issue.

This was the correct one: func applicationDidFinishLaunching(_ application: UIApplication) {

I'm not exactly sure why that caused the window creation loop but two days is enough time to waste on tracking that down. Hope that helps someone out there.

Joel Klabo
  • 253
  • 2
  • 12