SwiftUI中使用.viewAligned滚动目标行为的ScrollView点击按钮无法滚动到正确索引的问题
SwiftUI中使用.viewAligned滚动目标行为的ScrollView点击按钮无法滚动到正确索引的问题
我仔细看了你的代码和问题描述,手动滚动时按钮和卡片的同步没问题,但点击按钮时滚动位置出现错位,这其实是.viewAligned滚动目标行为的默认对齐逻辑和你设置的视图padding、卡片布局不匹配导致的。
问题根源
默认的.viewAligned行为是将滚动目标视图的边缘与ScrollView的容器边缘对齐,但你给ScrollView加了safeAreaPadding(.horizontal, 20),同时卡片用了containerRelativeFrame设置为容器宽度的98%,这就导致滚动目标的计算位置和你预期的卡片中心位置不一致。点击按钮设置scrollPosition时,ScrollView会滚动到目标视图的边缘对齐位置,而不是你想要的中心对齐,最终出现索引错位的问题。
解决方案
我们可以自定义一个滚动目标行为,让滚动目标对齐到卡片的中心位置,同时适配你设置的水平padding。具体步骤如下:
- 创建自定义的
ScrollTargetBehavior,调整滚动目标的位置,让卡片中心与ScrollView的可视区域中心对齐 - 替换原来的
.viewAligned行为为自定义的行为 - 确保
scrollPosition和selectedButtonsIndex的同步逻辑保持正确
修改后的完整代码如下:
import SwiftUI struct ContentView: View { let items = Array(1...8) @State private var selectedButtonsIndex: Int? = 0 @State private var scrollPosition: Int? = 0 // 自定义中心对齐的滚动目标行为 struct CenterAlignedScrollTargetBehavior: ScrollTargetBehavior { let horizontalPadding: CGFloat func updateTarget(_ target: inout ScrollTarget, context: TargetContext) { // 计算容器可视区域的中心位置 let containerCenterX = context.containerSize.width / 2 // 计算目标视图的中心位置 let targetCenterX = target.rect.midX // 计算需要调整的偏移量,让目标视图中心与容器中心对齐,同时考虑padding let offset = containerCenterX - targetCenterX - horizontalPadding target.rect.origin.x += offset } } var body: some View { VStack(spacing: 10) { // Buttons Collection LazyHStack(spacing: 15) { ForEach(Array(items.enumerated()), id: \.element) { index, item in Button(action: { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { selectedButtonsIndex = index } }) { Text("\(item)") .font(.title2.bold()) .foregroundColor(selectedButtonsIndex == index ? .white : .blue) .frame(width: 30, height:30) .background( Circle() .fill(selectedButtonsIndex == index ? Color.blue : Color.blue.opacity(0.2)) ) } .buttonStyle(PlainButtonStyle()) } } .padding(.horizontal) // Cells Collection ScrollView(.horizontal) { LazyHStack(spacing: 10) { ForEach(Array(items.enumerated()), id: \.element) { index, item in CardView(number: item, isSelected: selectedButtonsIndex == index) .containerRelativeFrame(.horizontal) { width, axis in return width * 0.98 } .id(index) .onTapGesture { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { selectedButtonsIndex = index } } } } .scrollTargetLayout() } // 使用自定义的中心对齐滚动行为,传入设置的水平padding值 .scrollTargetBehavior(CenterAlignedScrollTargetBehavior(horizontalPadding: 20)) .safeAreaPadding(.horizontal, 20) .scrollPosition(id: $scrollPosition) .onChange(of: selectedButtonsIndex) { _, newValue in if let newIndex = newValue { withAnimation(.spring(response: 0.65, dampingFraction: 0.82)) { scrollPosition = newIndex } } } .onChange(of: scrollPosition) { oldValue, newValue in if newValue != selectedButtonsIndex { selectedButtonsIndex = newValue } } HStack { ForEach(0..<items.count, id: \.self) { index in Circle() .fill(index == selectedButtonsIndex ? Color.blue : Color.gray.opacity(0.3)) .frame(width: 8, height: 8) } } } .padding(.vertical) } } struct CardView: View { let number: Int let isSelected: Bool var body: some View { RoundedRectangle(cornerRadius: 25) .fill( LinearGradient( colors: isSelected ? [.blue, .purple] : [.gray, .blue.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay( Text("\(number)") .font(.system(size: 80, weight: .bold)) .foregroundColor(.white) ) } }
为什么这样改?
- 自定义的
CenterAlignedScrollTargetBehavior会调整每个滚动目标的位置,让卡片的中心与ScrollView的可视区域中心对齐,同时考虑了你设置的20水平padding,这样点击按钮设置scrollPosition时,ScrollView会准确滚动到目标卡片的中心位置,不会出现错位。 - 保持了你原来的动画效果和手动滚动的同步逻辑,不会影响原来正常的交互。
你可以试试这段代码,应该就能解决按钮点击时滚动错位的问题了。
备注:内容来源于stack exchange,提问作者Darius
相关产品推荐
相关产品推荐

