How to navigate between multiple horizontal list with SwiftUI on tvOS

Viewed 839

I have multiple scrollable horizontal list in a global vertical list and I have difficulties to manage focus change.

focus issue

I have two major issue :

  • When a view don't have another view below, the bottom gesture don't update focus. I would like the focus to move to the near below view. In my GIF example, from fourth view in Section 1 to Section 2 second view.

  • When an view don't have an another view directly below, the bottom gesture move focus to the view that is vertically below. I would like the focus to move to the nearest bottom view. In my example from Section 1 third view to Section 2 second view instead of Section 3 third view.

I have many example of this issue in my tvOS app that I have fixed by using the combination of @Environment(\.resetFocus) var resetFocus and the prefersDefaultFocus viewModifier but I'm struggling with this use case.

I have also see another similar use case on this question but there is no answer yet.

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {

            Text("Section 1")

            ScrollView(.horizontal) {
                HStack() {
                    ForEach(0 ..< 10) { item in
                        Button(action: {
                            
                        }) {
                            FocusableRectangle()
                        }.buttonStyle(CardButtonStyle())
                    }
                }.padding(40)
            }
            
            Text("Section 2")

            ScrollView(.horizontal) {
                HStack() {
                    ForEach(0 ..< 2) { item in
                        Button(action: {
                            
                        }) {
                            FocusableRectangle()
                        }.buttonStyle(CardButtonStyle())
                    }
                }.padding(40)
            }
            
            Text("Section 3")

            ScrollView(.horizontal) {
                HStack() {
                    ForEach(0 ..< 3) { item in
                        Button(action: {
                            
                        }) {
                            FocusableRectangle()
                        }.buttonStyle(CardButtonStyle())
                    }
                }.padding(40)
            }
            
            Spacer()
        }
    }
}

struct FocusableRectangle: View {
    @Environment(\.isFocused) var isFocused: Bool
    @State var color = Color.blue
    
    var body: some View {
        Rectangle()
            .fill(color)
            .frame(width: 300.0, height: 200.0)
            .onChange(of: isFocused, perform: { value in
                color = value ? Color.red : Color.blue
            })
    }
}
2 Answers

You need to insert each section into a container, like Group or ZStack. Then, you can try override the .onMoveCommand with .up and .down directions in order to set the currently selected section. Now, you can set .prefersDefaultFocus with a condition for every section. You also need to call resetFocus when selection changes.

struct ContentView: View {
    @Namespace var mainNamespace
    @Environment(\.resetFocus) var resetFocus
    
    @State private var selectedSection = SelectedSection.sec_1
    
    var body: some View {
        VStack(alignment: .leading) {

            Text("Section 1")

            Group(){
                ScrollView(.horizontal) {
                    HStack() {
                        ForEach(0 ..< 10) { item in
                            Button(action: {
                                
                            }) {
                                FocusableRectangle()
                            }.buttonStyle(CardButtonStyle())
                        }
                    }.padding(40)
                }
            }.onMoveCommand(){direction in
                print(direction)
                if direction == .down{
                    self.selectedSection = SelectedSection.sec_2
                }
            }.prefersDefaultFocus(self.selectedSection == SelectedSection.sec_1, in: mainNamespace)

            Text("Section 2")

            Group(){
                ScrollView(.horizontal) {
                    HStack() {
                        ForEach(0 ..< 2) { item in
                            Button(action: {
                                
                            }) {
                                FocusableRectangle()
                            }.buttonStyle(CardButtonStyle())
                        }
                    }.padding(40)
                }
            }.onMoveCommand(){direction in
                print(direction)
                if direction == .down{
                    self.selectedSection = SelectedSection.sec_3
                }
                if direction == .up{
                    self.selectedSection = SelectedSection.sec_1
                }
            }.prefersDefaultFocus(self.selectedSection == SelectedSection.sec_2, in: mainNamespace)

            Text("Section 3")

            Group(){
                ScrollView(.horizontal) {
                    HStack() {
                        ForEach(0 ..< 3) { item in
                            Button(action: {
                                
                            }) {
                                FocusableRectangle()
                            }.buttonStyle(CardButtonStyle())
                        }
                    }.padding(40)
                }
            }.onMoveCommand(){direction in
                print(direction)
                if direction == .up{
                    self.selectedSection = SelectedSection.sec_2
                }
            }.prefersDefaultFocus(self.selectedSection == SelectedSection.sec_3, in: mainNamespace)

            Spacer()
        }
        .focusScope(mainNamespace)
        .onAppear(){
            print(selectedSection)
        }
        .onChange(of: self.selectedSection){newValue in
            resetFocus(in: mainNamespace)
        }
    }
}

I was wondering if you had tried adding .focusSection() that appear with tvOS 15 on your different ScrollView Sections?

This is how your ContentView would look like:

struct ContentView: View {

var body: some View {
VStack(alignment: .leading) {
            Text("Section 1")

            ScrollView(.horizontal) {
                HStack {
                    ForEach(0..<10) { _ in
                        Button(action: {}) {
                            FocusableRectangle()
                        }.buttonStyle(CardButtonStyle())
                    }
                }.padding(40)
            }.focusSection()

            Text("Section 2")

            ScrollView(.horizontal) {
                HStack {
                    ForEach(0..<2) { _ in
                        Button(action: {}) {
                            FocusableRectangle()
                        }.buttonStyle(CardButtonStyle())
                    }
                }.padding(40)
            }.focusSection()

            Text("Section 3")

            ScrollView(.horizontal) {
                HStack {
                    ForEach(0..<3) { _ in
                        Button(action: {}) {
                            FocusableRectangle()
                        }.buttonStyle(CardButtonStyle())
                    }
                }.padding(40)
            }.focusSection()

            Spacer()
        }
    }
}

It should do the trick and achieve an understandable focus pattern.

Hope it helps.

Related