SwiftUI tvOS: Handle focus for first button on navigating downwards direction

1.6k Views Asked by At

I want to focus on the first Button A of the bottom Hstack when the user navigates downwards. How can I achieve that? As of now, the guide is picking the nearest element.

Code:

import SwiftUI

struct DummyView: View {
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        contentView
        parent
    }

    private var parent: some View {
        VStack {
            if #available(tvOS 15.0, *) {
                HStack {
                    Spacer()
                    Button ("1") {}
                    Button ("2") {}
                    Button ("3") {}
                    Spacer()
                }
                .focusSection()
                .border(Color.white, width: 2)
            } else {
                // Fallback on earlier versions
            }

            Spacer()
            if #available(tvOS 15.0, *) {
                HStack {
                    Button ("A") {}
                    Spacer()
                    Button ("B") {}
                    Spacer()
                    Button ("C") {}
                }
                .border(Color.white, width: 2)
                .focusSection()
            } else {
                // Fallback on earlier versions
            }
        }
    }

    private var contentView: some View {
        VStack {
            Spacer()
            Text("THIS IS DUMMY SCREEN")
            Spacer()
        }
    }
}

Screenshot:

enter image description here

1

There are 1 best solutions below

0
On

I've used @FocusState to achieve this. You can bind the focus state of the view to an enum, then create an empty view to intercept/direct focus when it receives focus:

import SwiftUI

struct DummyView: View {
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        contentView
        parent
    }
    
    enum FocusAreas {
        case button1
        case button2
        case button3
        case focusGuide
        case buttonA
        case buttonB
        case buttonC
    }
    
    @FocusState var focusState: FocusAreas?

    private var parent: some View {
        VStack {
            if #available(tvOS 15.0, *) {
                HStack {
                    Spacer()
                    Button ("1") {}
                        .focused($focusState, equals: .button1)
                    Button ("2") {}
                        .focused($focusState, equals: .button2)
                    Button ("3") {}
                        .focused($focusState, equals: .button3)
                    Spacer()
                }
                .border(Color.white, width: 2)
            } else {
                // Fallback on earlier versions
            }
            Color.clear
                .frame(height: 1)
                .frame(maxWidth: .infinity)
                .focusable()
                .focused($focusState, equals: .focusGuide)
                .onChange(of: focusState, perform: {[focusState] newFocus in
                    if(newFocus == .focusGuide) {
                        switch(focusState){
                        case .button1,.button2,.button3:
                            self.focusState = .buttonA
                        default:
                            //Add custom behaviors when navigating up here from buttons A,B,C here
                            self.focusState = .button1
                            break
                        }
                    }
                })
            Spacer()
            if #available(tvOS 15.0, *) {
                HStack {
                    Button ("A") {}
                        .focused($focusState, equals: .buttonA
                        )
                    Spacer()
                    Button ("B") {}
                        .focused($focusState, equals: .buttonB)
                    Spacer()
                    Button ("C") {}
                        .focused($focusState, equals: .buttonC)
                }
                .border(Color.white, width: 2)
            } else {
                // Fallback on earlier versions
            }
        }
    }

    private var contentView: some View {
        VStack {
            Spacer()
            Text("THIS IS DUMMY SCREEN")
            Spacer()
        }
    }
}