SwiftUI:ScrollView嵌套LazyHStack致自定义滚轮选择器滚动异常
自定义横向滚轮选择器绑定UserDefaults的滚动与保存问题解决
问题复盘
- 使用普通
HStack时:滚动选择新值能触发UserDefaults保存,但视图初始显示始终定位到0(已确认传入值非0) - 使用
LazyHStack时:初始定位正常,但出现滚动定位偏移、滚动失效等异常 - 临时尝试方案:新增可选Int变量,通过
.task初始化绑定值并作为scrollPosition传入,同时给HStack加onTap更新绑定值与该变量——解决了滚动和初始定位问题,但UserDefaults不再保存值
问题根源
- 普通HStack初始定位失效:
ScrollView首次渲染时,HStack全部子视图未完成布局,导致滚动位置无法正确计算并定位到初始值 - LazyHStack滚动异常:直接将原
@Binding包装成可选类型传入scrollPosition,与LazyHStack的懒加载特性冲突,引发滚动位置更新和视图渲染不同步,出现定位偏差、滚动失效 - 临时方案保存失效:新增变量脱离了原
@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
相关产品推荐
相关产品推荐

