3

The onFocusChange closure in the focusable(_:onFocusChange:) modifier allows me to set properties for the parent view when child views are focused, like this:

struct ContentView: View {
    @State var text: String
    var body: some View {
        VStack {
            Text(text)
            Text("top")
                .padding()
                .focusable(true, onFocusChange: { focused in
                    text = "top focus"
                })
            Text("bottom")
                .padding()
                .focusable(true, onFocusChange: { focused in
                    text = "bottom focus"
                })
        }
        
    }
}

But in the 2020 WWDC video where focusable is introduced, it is clearly stated that this wrapper in not intended to be used with intrinsically focusable views such as Buttons and Lists. If I use Button in place of Text here the onFocusChange works, but the normal focus behaviour for the Buttons breaks:

struct ContentView: View {
    @State var text: String
    var body: some View {
        VStack {
            Text(text)
            Button("top") {}
                .padding()
                .focusable(true, onFocusChange: { focused in
                    text = "top focus"
                })
            Button("bottom") {}
                .padding()
                .focusable(true, onFocusChange: { focused in
                    text = "bottom focus"
                })
        }
        
    }
}

Is there any general way to get an onFocusChange closure to use with Buttons that doesn't break their normal focusable behaviour? Or is there some other way to accomplish this?

c_booth
  • 2,185
  • 1
  • 13
  • 22
  • Does this answer your question https://stackoverflow.com/a/63172333/12299030? – Asperi Apr 14 '21 at 13:44
  • @Asperi, Unfortunately, no. With @Environment(\.isFocused) var focused: Bool, I can declaratively modify how views appear, i.e. with ternary conditionals, but this doesn't provide any way to assign a value or execute code based focus change. – c_booth Apr 14 '21 at 13:50
  • 1
    Try using the .isFocused environment value with .onChange(of:perform:) to execute code. https://developer.apple.com/documentation/swiftui/text/onchange(of:perform:) – Adam Apr 14 '21 at 17:23
  • @Adam, thank you! .onChange(of:perform:) with .isFocused environment value gives me the callback I needed. If you have the chance, please add this as an answer – c_booth Apr 14 '21 at 18:09

1 Answers1

5

Try using @Environment(\.isFocused) and .onChange(of:perform:) in a ButtonStyle:

struct ContentView: View {
    var body: some View {
          Button("top") {
             // button action
          }
          .buttonStyle(MyButtonStyle())
    }
}

struct MyButtonStyle: ButtonStyle {
    @Environment(\.isFocused) var focused: Bool

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: focused) { newValue in
                // do whatever based on focus
            }
    }
}

IIRC using @Environment(\.isFocused) inside a ButtonStyle may only work on iOS 14.5+, but you could create a custom View instead of a ButtonStyle to support older versions.

Adam
  • 4,405
  • 16
  • 23