ScrollView and prefersDefaultFocus currently incompatible?

Hello,

I just wanted to confirm that ScrollView and prefersDefaultFocus are currently not compatible, at least when building for tvOS

A simple example:

    HStack {
      ScrollViewReader { proxy in
        ScrollView(.horizontal, showsIndicators: true) {
          VStack {
            Button(action: {
              resetFocus(in: namespace)
            }) {
              Text("RESET FOCUS")
            }
            HStack {
              ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], id:  \.self) { v in
                Button(action: {}) {
                  Text("ABC \(v)")
                }.prefersDefaultFocus(v == 11, in: namespace)
              }
            }
          }.onAppear {
            resetFocus(in: namespace)

          }
           
        }
      }
      .focusScope(namespace)
    }
  }

On load of the view the focus remains on the first button in the HStack. On pressing the RESET FOCUS button focus is applied to the first button visible on the left side of the screen.

Am I missing something here? Is this the way these pieces of SwiftUI currently interact? e.g. prefersDefaultFocus has no effect inside a ScrollView?

Is there a way to focus a specific item in a ScrollView programmatically?

Thanks.

Yes, seems like since you wrap up HStack to ScrollView prefersDefaultFocus won't work correctly after that. I have a similar issue and can't get it to work for tvOS 14. Nevertheless, it works well with iOS 15 using @FocusState. So if you don't need tvOS 14 support you can use that approach. Here are my observations of the issue. What I try to achieve is set focus on the last item(10) if you press down on items 7 or 8. If I'm not wrapping up LazyVGrid into ScrollView everything works as expected. But as soon as I wrap up LazyVGrid into ScrollView when I press the down button on those items it always gets focused on the first item. Here is my code example and screenshot.

struct ContentView: View {
    @Namespace var focusNamespace
    @Environment(\.resetFocus) var resetFocus
    @State private var defaultFocusIndex: Int = 0
    @State var items = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

    let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)

    var body: some View {

        ScrollView() {
            LazyVGrid(columns: columns) {
                ForEach(items, id: \.self) { item in
                    Button {

                    } label: {
                        ItemView(title: item)
                    }
                    .prefersDefaultFocus(item == items[defaultFocusIndex], in: focusNamespace)
                    .onMoveCommand { moveCommandDirection in
                        guard let index = items.firstIndex(where: {$0 == item}) else {
                            return
                        }

                        if moveCommandDirection == .down, index >= items.count - columns.count {
                            defaultFocusIndex = items.count - 1
                            resetFocus(in: focusNamespace)
                        }
                    }
                }
            }
            .focusScope(focusNamespace)
        }
    }
}

And here is the working solution for tvOS 15:

struct ContentView: View {
    @FocusState var focusedItem: String?
    @State var items = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
    let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)

    var body: some View {
        ScrollView() {
            LazyVGrid(columns: columns) {
                ForEach(items, id: \.self) { item in
                    Button {

                    } label: {
                        ItemView(title: item)
                    }
                    .focused($focusedItem, equals: item)
                    .onMoveCommand { moveCommandDirection in
                        guard let index = items.firstIndex(where: {$0 == item}) else {
                            return
                        }
                        if moveCommandDirection == .down, index >= items.count - columns.count {
                            focusedItem = items.last
                        }
                    }
                    .padding(.bottom, 8)
                }
            }
        }
    }
}

If you have any workaround for tvOS 14 I'll be appreciated it.

Does this approach still work for you? I try it on a tvOS 17 simulator, and it always resets to the first item, even if I say that the default focus index should be 2.

ScrollView and prefersDefaultFocus currently incompatible?
 
 
Q