1

Aim

  • To use a keyboard shortcut on a Multiplatform app (iOS and macOS)
  • When keyboard shortcut is tapped, the price needs to be printed

Problem

  • The same code doesn't work on iPad but works on macOS

Question:

  • Why is not working?
  • How to fix this?

Environment

  • macOS Big Sur - 11.6 (20G165)
  • Version 13.0 (13A233)

Code

@main
struct TestApp: App {
    @State private var price = 0

    var body: some Scene {
        WindowGroup {
            ContentView(price: $price)
                .focusedValue(\.price, $price)
        }
        .commands {
            PriceCommands()
        }
    }
}

struct ContentView: View {
    
    @Binding var price: Int
    
    var body: some View {
        IncreasePriceButton(price: $price)
    }
}

struct IncreasePriceButton: View {
    
    @Binding var price: Int
    
    var body: some View {
        Button("Increase Price") {
            price += 1
            print("price = \(price)")
        }
    }
}

struct PriceCommandButton: View {
    
    @FocusedBinding(\.price) var price
    
    var body: some View {
        Button("Print Price") {
            print("price = \(price)")
        }
    }
}

struct PriceCommands: Commands {
    var body: some Commands {
        CommandMenu("Custom") {
            PriceCommandButton()
                .keyboardShortcut(KeyboardShortcut("C", modifiers: [.command, .shift]))
        }
    }
}

struct FocusedPriceKey: FocusedValueKey {
    typealias Value = Binding<Int>
}

extension FocusedValues {
    var price: FocusedPriceKey.Value? {
        get { self[FocusedPriceKey.self] }
        set { self[FocusedPriceKey.self] = newValue }
    }
}
user1046037
  • 16,755
  • 12
  • 92
  • 138

1 Answers1

1

So I took the demo code and added a little bit of debug as shown below.

As far as I can tell, there are two problems.

First that focusedValue on ContentView was not being set because the View is not being focused. Which can be seen because the @FocusBinding in OtherView doesn't pick up the price value as expected.

This could be a SwiftUI bug, but it's easy to work around by switching over to the focusedSceneValue modifier. Anyway, with that in place the price in ContentView and OtherView keeps in step.

The second problem is that even with the first fix/workaround in place, changes in the price are never causing the Custom menu to be rebuilt after it's initial rendering.

I think this is a bug with SwiftUI and would recommend submitting a feedback submission to Apple letting them know about it.

The workaround (in this case) with price being app global state, would be to just pass it through to PriceCommands as an argument (as is already being done for ContentView).

Kind regards

import SwiftUI

@main
struct TestApp: App {
    @State private var price = 0

    var body: some Scene {
        WindowGroup {
            ContentView(price: $price)
                .focusedSceneValue(\.price, $price) // <== Changed
        }
        .commands {
            PriceCommands()
        }
    }
}

struct OtherView: View {
    @FocusedBinding(\.price) var price: Int?

    var body: some View {
        Text("OtherView price = \(String(describing: price))")
    }
}

struct ContentView: View {
    @Binding var price: Int

    var body: some View {
        VStack {
            OtherView()
            IncreasePriceButton(price: $price)
        }
    }
}

struct IncreasePriceButton: View {
    @Binding var price: Int

    var body: some View {
        Button("Increase Price") {
            price += 1
            print("price = \(price)")
        }
    }
}

struct PriceCommandButton: View {
    @FocusedBinding(\.price) var price
    var priceText: String {
        String(price ?? -1)
    }

    var body: some View {
        print("Buiding commands menu with \(String(describing: price))")
        return Button("Print Price \(priceText)") {
            print("price = \(String(describing: price))")
        }
    }
}

struct PriceCommands: Commands {
    var body: some Commands {
        CommandMenu("Custom") {
            PriceCommandButton()
                .keyboardShortcut(KeyboardShortcut("C", modifiers: [.command, .shift]))
        }
    }
}

struct FocusedPriceKey: FocusedValueKey {
    typealias Value = Binding<Int>
}

extension FocusedValues {
    var price: FocusedPriceKey.Value? {
        get { self[FocusedPriceKey.self] }
        set { self[FocusedPriceKey.self] = newValue }
    }
}
shufflingb
  • 1,847
  • 16
  • 21
  • 1
    Thanks for your response, the downside of maintaining state in the App is that there could be multiple windows in macOS / iPad, the price is just an example, I wouldn't want it to be the same across different scenes, it needs to be local to the scene. I have filed a feedback, no response on the feedback yet. – user1046037 Oct 15 '21 at 00:26
  • 2
    Yes, I get app state vs scene state :-( Didn't mention in the answer but have been playing around on macOS trying to pass state from WindowGroup instances to Commands menu. Afraid on macOS I think it's also broken (feedback submitted). Specifically not restoring focusedValues when the window gets keyWindow status again and doesn't always send the correct value i.e. that from the keyWindow, to the Command menu builder. I've got hacky workarounds, which I'll post a link to here once the codes cleaned up, but hopefully Apple will fix. – shufflingb Oct 15 '21 at 08:57
  • In case you want the macOS implementation, the original code I posted works, thanks again – user1046037 Oct 15 '21 at 12:29
  • 1
    @user1046037 - No probs. My hack around the problems with building the command menus and restoring focusedValue state on macOS is here https://github.com/shufflingB/swiftui-hack-macos-focusedVals . Good luck with the projects :-) – shufflingb Oct 15 '21 at 14:03
  • Glad to know you have build a workaround in the interim. Just curious would `@Environment(\.isFocused)` be useful in your solution? – user1046037 Oct 15 '21 at 23:56
  • 1
    @user1046037 - `isFocused` - tried a couple of times previously, no joy for me. Right though, does sounds like it should do something similiar to the hack's keyWindow finder. Failure may be me missunderstanding usage, or may be it being broken by same bug(s) that are causing the need for the hack in the first place ... Not sure, but will update here when things have been cleared up (either the bug, or my understanding of how things are supposed to work :-/ ). Kind regards. – shufflingb Oct 16 '21 at 11:14