iosswiftuiscrollviewhstackscrollviewreader

ScrollViewReader ScrollTo notWorking


I'm creating a horizontal menu and I'm using horizontal ScrollView and an HStack to view its items. Everything seems to work but I have a problem with ScrollViewReader.. As you should from the code when currentIndex changes I pass its value to the proxy.scrollTo() method but I don't get any results, the ScrollView does not follow the modification of currentIndex remaining still in its original position. Can you tell me where I'm going wrong? thank you all

struct ScrollableTabMenu<T:CaseIterable & Hashable & Identifiable>: View  {
    var items: [T]
    var title: KeyPath<T, String>
    @Binding var currentIndex: Int
    @Namespace private var animation
    
    var body: some View {
                  
        ScrollView(.horizontal) {
            ScrollViewReader { proxy in
                HStack {
                    ForEach(items) { screen in
                        let index = items.firstIndex(of: screen)!
                        Button {
                            currentIndex = index
                        } label: {
                            Text(screen[keyPath: title])
                                .padding()
                                .overlay(alignment: .bottom){
                                    if currentIndex == index  {
                                        RoundedRectangle(cornerRadius: 6)
                                            .frame(height: 1)
                                            .matchedGeometryEffect(id: "AninamtionTAB", in: animation)
                                    }
                                }
                        }
                        .buttonStyle(.plain)
                    }
                }
                .onChange(of: currentIndex, { _, newValue in
                    withAnimation {
                        proxy.scrollTo(newValue, anchor: .leading)
                        print(newValue)
                    }
                })
            }
        }
        .scrollTargetBehavior(.paging)
        .scrollIndicators(.hidden)
        .contentMargins(.horizontal, 16)
    }
}

Solution

  • scrollTo takes the id of the view that you are scrolling to. All your buttons have the id that is automatically given by ForEach, which is the corresponding value of T in items, not an Int index number.

    If you want to use the index as the id, you should add .id(index) to the buttons.

    However, indices are rather unstable - they change whenever you add/remove a tab. I would suggest using a Binding<T> to represent the current tab.

    @Binding var currentTab: T
    var body: some View {
                  
        ScrollView(.horizontal) {
            ScrollViewReader { proxy in
                HStack {
                    ForEach(items) { screen in
                        let index = items.firstIndex(of: screen)!
                        Button {
                            currentTab = screen
                        } label: {
                            Text(screen[keyPath: title])
                                .padding()
                                .overlay(alignment: .bottom){
                                    if currentTab == screen  {
                                        RoundedRectangle(cornerRadius: 6)
                                            .frame(height: 1)
                                            .matchedGeometryEffect(id: "AninamtionTAB", in: animation)
                                    }
                                }
                        }
                        .buttonStyle(.plain)
                    }
                }
                .onChange(of: currentTab, { _, newValue in
                    withAnimation {
                        proxy.scrollTo(newValue, anchor: .leading)
                    }
                })
    

    Note that now you don't even need to add your own .id(screen) - the id that ForEach automatically assigns to the buttons is exactly the id that we want them to have.

    Using the .paging scroll behaviour is rather weird in this context. The pages doesn't necessarily align with the tab buttons, so when you tap on a tab, the button gets scrolled to the left, and might get "cut off", because that happens to be where a new "page" begins.

    I prefer using .scrollTargetBehavior(.viewAligned), and setting the HStack as the .scrollTargetLayout().

    I would also not use a KeyPath<T, String>. This doesn't allow for localisation. I would take in a (T) -> Text or (T) -> Label where Label is a generic type parameter constrained to View.