Search code examples
swiftuifocustvosfocus-engine

Getting onFocusChange callback for Buttons in SwiftUI (tvOS)


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?


Solution

  • 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.