You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中使用.viewAligned滚动目标行为的ScrollView点击按钮无法滚动到正确索引的问题

SwiftUI中使用.viewAligned滚动目标行为的ScrollView点击按钮无法滚动到正确索引的问题

我仔细看了你的代码和问题描述,手动滚动时按钮和卡片的同步没问题,但点击按钮时滚动位置出现错位,这其实是.viewAligned滚动目标行为的默认对齐逻辑和你设置的视图padding、卡片布局不匹配导致的。

问题根源

默认的.viewAligned行为是将滚动目标视图的边缘与ScrollView的容器边缘对齐,但你给ScrollView加了safeAreaPadding(.horizontal, 20),同时卡片用了containerRelativeFrame设置为容器宽度的98%,这就导致滚动目标的计算位置和你预期的卡片中心位置不一致。点击按钮设置scrollPosition时,ScrollView会滚动到目标视图的边缘对齐位置,而不是你想要的中心对齐,最终出现索引错位的问题。

解决方案

我们可以自定义一个滚动目标行为,让滚动目标对齐到卡片的中心位置,同时适配你设置的水平padding。具体步骤如下:

  1. 创建自定义的ScrollTargetBehavior,调整滚动目标的位置,让卡片中心与ScrollView的可视区域中心对齐
  2. 替换原来的.viewAligned行为为自定义的行为
  3. 确保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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:28:12