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

SwiftUI:ScrollView嵌套LazyHStack致自定义滚轮选择器滚动异常

自定义横向滚轮选择器绑定UserDefaults的滚动与保存问题解决

问题复盘

  • 使用普通HStack时:滚动选择新值能触发UserDefaults保存,但视图初始显示始终定位到0(已确认传入值非0)
  • 使用LazyHStack时:初始定位正常,但出现滚动定位偏移、滚动失效等异常
  • 临时尝试方案:新增可选Int变量,通过.task初始化绑定值并作为scrollPosition传入,同时给HStack加onTap更新绑定值与该变量——解决了滚动和初始定位问题,但UserDefaults不再保存值

问题根源

  1. 普通HStack初始定位失效:ScrollView首次渲染时,HStack全部子视图未完成布局,导致滚动位置无法正确计算并定位到初始值
  2. LazyHStack滚动异常:直接将原@Binding包装成可选类型传入scrollPosition,与LazyHStack的懒加载特性冲突,引发滚动位置更新和视图渲染不同步,出现定位偏差、滚动失效
  3. 临时方案保存失效:新增变量脱离了原@Binding的didSet触发链路,更新变量时不会触发模型属性的didSet,因此UserDefaults无写入操作

解决方案

核心思路

  • 视图内部维护独立的滚动位置状态,避免直接绑定外部值引发的Lazy布局冲突
  • 同步内部滚动状态与外部绑定值,确保双向更新时触发模型的didSet,完成UserDefaults保存
  • 利用.task在视图加载完成后设置初始滚动位置,解决初始定位问题

修改后的代码

滚轮选择器视图(WheelPicker)

struct WheelPicker: View {
    var count: Int
    var spacing: CGFloat = 80
    @Binding var value: Int
    
    // 内部维护滚动位置状态,避免直接绑定外部值引发的布局冲突
    @State private var scrollPosition: Int?
    
    var body: some View {
        GeometryReader { geometry in
            let horizontalPadding = geometry.size.width / 2
            
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(spacing: spacing) {
                    ForEach(0..<count, id: \.self) { index in
                        Divider()
                            .foregroundStyle(.blue)
                            .frame(width: 0, height: 30, alignment: .center)
                            .frame(maxHeight: 30, alignment: .bottom)
                            .overlay(alignment: .bottom) {
                                Text("\(index)")
                                    .font(.system(size: index == value ? 25 : 20))
                                    .textScale(.secondary)
                                    .fixedSize()
                                    .offset(y: index == value ? 43 : 38)
                            }
                    }
                }
                .scrollTargetLayout()
            }
            .scrollIndicators(.hidden)
            .scrollTargetBehavior(.viewAligned)
            // 绑定内部滚动位置状态
            .scrollPosition(id: $scrollPosition)
            // 同步滚动位置到外部绑定值,触发模型didSet完成UserDefaults保存
            .onChange(of: scrollPosition) { newValue in
                guard let newValue, newValue != value else { return }
                value = newValue
            }
            // 视图加载完成后设置初始滚动位置,解决LazyHStack初始定位问题
            .task {
                scrollPosition = value
            }
            .overlay(alignment: .center) {
                Rectangle()
                    .frame(width: 1, height: 45)
            }
            .safeAreaPadding(.horizontal, horizontalPadding)
        }
    }
}

#Preview {
    @Previewable @State var count: Int = 30
    @Previewable @State var value: Int = 5
    WheelPicker(count: count, value: $value)
}

模型层优化(可选)

如果模型初始化时未从UserDefaults读取已保存的值,建议补充初始化逻辑:

class YourViewModel: ObservableObject {
    @Published var captureInterval: Int = UserDefaults.standard.integer(forKey: "captureInterval") == 0 ? 1 : UserDefaults.standard.integer(forKey: "captureInterval") {
        didSet {
            UserDefaults.standard.set(captureInterval, forKey: "captureInterval")
        }
    }
    
    @Published var startCaptureInterval: Int = UserDefaults.standard.integer(forKey: "startCaptureInterval") == 0 ? 2 : UserDefaults.standard.integer(forKey: "startCaptureInterval") {
        didSet {
            UserDefaults.standard.set(startCaptureInterval, forKey: "startCaptureInterval")
        }
    }
}

效果说明

  • 保留LazyHStack的懒加载特性,优化大量选项时的性能
  • 初始定位准确,滚动操作正常,滚动选择的新值能正确写入UserDefaults
  • 内部状态与外部绑定值双向同步,避免布局冲突与保存失效问题

内容的提问来源于stack exchange,提问作者john smith

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 18:00:57