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

SwiftUI中.viewAligned滚动目标行为导致离散Scrubber端点值异常的问题咨询

SwiftUI中.viewAligned滚动目标行为导致离散Scrubber端点值异常的问题咨询

我最近基于SwiftUI的ScrollView实现了一个垂直离散Scrubber,核心功能正常,但在处理端点值时出现了几个异常情况:

  • 直接滚动到底部时,控件显示的值为87,而非预期的100
  • 通过点击+按钮逐步递增到端点时,能正确显示100
  • 点击-按钮调整值时,Scrubber不会立即滚动到对应位置,需要连续点击两次才会跳转

我怀疑问题出在.viewAligned的滚动目标行为上,但无法定位具体的逻辑漏洞。以下是完整的实现代码:

import SwiftUI

struct ScrubberConfig {
    let count: Int
    let steps: Int
    let spacing: CGFloat
}

struct VerticalScrubber: View {
    var config: ScrubberConfig
    @Binding var value: CGFloat
    @State private var scrollPosition: Int?
    
    var body: some View {
        GeometryReader { geometry in
            let verticalPadding = geometry.size.height / 2 - 8
            
            ZStack(alignment: .trailing) {
                ScrollView(.vertical, showsIndicators: false) {
                    VStack(spacing: config.spacing) {
                        ForEach(0...(config.steps * config.count), id: \.self) { index in
                            horizontalTickMark(for: index)
                                .id(index)
                        }
                    }
                    .frame(width: 80)
                    .scrollTargetLayout()
                    .safeAreaPadding(.vertical, verticalPadding)
                }
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $scrollPosition, anchor: .top)
                
                Capsule()
                    .frame(width: 32, height: 3)
                    .foregroundColor(.accentColor)
                    .shadow(color: .accentColor.opacity(0.3), radius: 3, x: 0, y: 1)
            }
            .frame(width: 100)
            .onAppear {
                DispatchQueue.main.async {
                    scrollPosition = Int(value * CGFloat(config.steps))
                }
            }
            .onChange(of: value, { oldValue, newValue in
                let newIndex = Int(newValue * CGFloat(config.steps))
                print("New index \(newIndex)")
                if scrollPosition != newIndex {
                    withAnimation {
                        scrollPosition = newIndex
                        print("\(scrollPosition)")
                    }
                }
            })
            .onChange(of: scrollPosition, { oldIndex, newIndex in
                guard let pos = newIndex else { return }
                let newValue = CGFloat(pos) / CGFloat(config.steps)
                if abs(value - newValue) > 0.001 {
                    value = newValue
                }
            })
        }
    }
    
    private func horizontalTickMark(for index: Int) -> some View {
        let isMajorTick = index % config.steps == 0
        let tickValue = index / config.steps
        
        return HStack(spacing: 8) {
            Rectangle()
                .fill(isMajorTick ? Color.accentColor : Color.gray.opacity(0.5))
                .frame(width: isMajorTick ? 24 : 12, height: isMajorTick ? 2 : 1)
            
            if isMajorTick {
                Text("\(tickValue * 5)")
                    .font(.system(size: 12, weight: .medium))
                    .foregroundColor(.primary)
                    .fixedSize()
            }
        }
        .frame(maxWidth: .infinity, alignment: .trailing)
        .padding(.trailing, 8)
    }
}

#Preview("Vertical Scrubber") {
    struct VerticalScrubberPreview: View {
        @State private var value: CGFloat = 0
        private let config = ScrubberConfig(count: 20, steps: 5, spacing: 8)
        
        var body: some View {
            VStack {
                Text("Vertical Scrubber (0–100 in steps of 5)")
                    .font(.title2)
                    .padding()
                
                HStack(spacing: 30) {
                    VerticalScrubber(config: config, value: $value)
                        .frame(width: 120, height: 300)
                        .background(Color(.systemBackground))
                        .border(Color.gray.opacity(0.3))
                    
                    VStack {
                        Text("Current Value:")
                            .font(.headline)
                        
                        Text("\(value * 5, specifier: "%.0f")")
                            .font(.system(size: 36, weight: .bold))
                            .padding()
                        
                        HStack {
                            Button("−5") {
                                let newValue = max(0, value - 1)
                                if value != newValue {
                                    value = newValue
                                    UISelectionFeedbackGenerator().selectionChanged()
                                }
                                print("Value \(newValue), \(value)")
                            }
                            .disabled(value <= 0)
                            
                            Button("+5") {
                                let newValue = min(CGFloat(config.count), value + 1)
                                if value != newValue {
                                    value = newValue
                                    UISelectionFeedbackGenerator().selectionChanged()
                                }
                                print("Value \(newValue), \(value)")
                            }
                            .disabled(value >= CGFloat(config.count))
                        }
                        .buttonStyle(.bordered)
                    }
                }
                
                Spacer()
            }
            .padding()
        }
    }
    
    return VerticalScrubberPreview()
}

我尝试过在onAppear和onChange中同步scrollPosition与value的关联,但端点的异常问题依然存在。有没有人能帮我分析下.viewAligned在这里的具体作用逻辑,以及如何修复这些端点行为问题?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 11:08:02