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)
}
}
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
.