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

SwiftUI中如何为路径设置Double类型上下界以定位视图?

实现带数值范围的路径跟随视图

当然可以实现!核心思路是把路径的总长度映射到你设定的lowerBound到upperBound数值范围,然后根据currentValue计算出对应的路径百分比,再获取该百分比位置的坐标和切线角度,就能定位并旋转任意SwiftUI视图。

关键步骤拆解

  • 计算路径总长度
    先通过Path的cgPath获取总长度,这是数值范围和路径位置映射的基础:

    extension Path {
        var totalLength: CGFloat {
            var length: CGFloat = 0
            let path = cgPath
            path.applyWithBlock { element in
                guard let points = element.pointee.points else { return }
                switch element.pointee.type {
                case .moveToPoint:
                    break
                case .addLineToPoint:
                    length += hypot(points[0].x - points[1].x, points[0].y - points[1].y)
                case .addQuadCurveToPoint:
                    // 贝塞尔曲线近似长度计算,也可用更精确的分段计算
                    let start = points[0], control = points[1], end = points[2]
                    let a = hypot(start.x - 2*control.x + end.x, start.y - 2*control.y + end.y)
                    let b = hypot(2*control.x - 2*start.x, 2*control.y - 2*start.y)
                    length += (a + b)/2
                case .addCurveToPoint:
                    // 三次贝塞尔曲线同理,用近似或分段计算
                    let start = points[0], c1 = points[1], c2 = points[2], end = points[3]
                    let a = hypot(start.x - 3*c1.x + 3*c2.x - end.x, start.y - 3*c1.y + 3*c2.y - end.y)
                    let b = hypot(3*c1.x - 3*start.x, 3*c1.y - 3*start.y)
                    let c = hypot(3*c2.x - 3*c1.x, 3*c2.y - 3*c1.y)
                    length += (a + b + c)/3
                case .closeSubpath:
                    break
                @unknown default:
                    break
                }
            }
            return length
        }
    }
    
  • 根据数值计算路径上的位置
    把currentValue归一化到0-1的百分比,再通过CGPath的截断方法获取对应位置的坐标:

    func point(on path: Path, at percentage: CGFloat) -> CGPoint {
        let clampedPercentage = min(max(percentage, 0), 1)
        let trimmedPath = path.cgPath.copy(trimmedFrom: 0, to: clampedPercentage) ?? path.cgPath
        var endPoint = CGPoint.zero
        trimmedPath.applyWithBlock { element in
            switch element.pointee.type {
            case .moveToPoint, .addLineToPoint:
                endPoint = element.pointee.points![0]
            case .addQuadCurveToPoint:
                endPoint = element.pointee.points![2]
            case .addCurveToPoint:
                endPoint = element.pointee.points![3]
            default:
                break
            }
        }
        return endPoint
    }
    
  • 获取路径切线角度(可选,用于视图旋转)
    如果需要视图沿路径方向自动旋转,可以计算对应位置的切线角度:

    func angle(on path: Path, at percentage: CGFloat) -> Angle {
        let clampedPercentage = min(max(percentage, 0), 1)
        let delta = 0.001 // 取极短一段路径计算切线
        let startPercent = max(clampedPercentage - delta, 0)
        let endPercent = min(clampedPercentage + delta, 1)
        
        let startPoint = point(on: path, at: startPercent)
        let endPoint = point(on: path, at: endPercent)
        
        let dx = endPoint.x - startPoint.x
        let dy = endPoint.y - startPoint.y
        let angle = atan2(dy, dx)
        return Angle(radians: Double(angle))
    }
    
  • 封装成可复用的PathFollowView
    把逻辑封装成通用视图,支持自定义路径、数值范围、当前值和跟随视图:

    struct PathFollowView<Content: View>: View {
        let path: Path
        let lowerBound: CGFloat
        let upperBound: CGFloat
        let currentValue: CGFloat
        let content: () -> Content
        
        private var percentage: CGFloat {
            guard upperBound != lowerBound else { return 0 }
            let normalizedValue = currentValue - lowerBound
            let range = upperBound - lowerBound
            return normalizedValue / range
        }
        
        var body: some View {
            ZStack {
                path.stroke() // 可选:显示路径作为参考
                
                content()
                    .position(point(on: path, at: percentage))
                    .rotationEffect(angle(on: path, at: percentage))
            }
        }
    }
    

使用示例

struct ContentView: View {
    @State private var currentValue = 25.0
    
    var body: some View {
        PathFollowView(
            path: makeCustomPath(),
            lowerBound: 0,
            upperBound: 100,
            currentValue: currentValue,
            content: {
                Image(systemName: "car.fill")
                    .foregroundColor(.red)
                    .font(.title)
            }
        )
        .onAppear {
            // 测试动画:手动修改currentValue即可触发位置更新,无持续动画问题
            withAnimation(.easeInOut(duration: 3)) {
                currentValue = 100
            }
        }
    }
    
    func makeCustomPath() -> Path {
        var path = Path()
        path.move(to: CGPoint(x: 50, y: 50))
        path.addQuadCurve(to: CGPoint(x: 300, y: 50), control: CGPoint(x: 175, y: 150))
        path.addLine(to: CGPoint(x: 300, y: 250))
        path.addCurve(to: CGPoint(x: 50, y: 250), control1: CGPoint(x: 225, y: 350), control2: CGPoint(x: 125, y: 350))
        return path
    }
}

解决持续动画问题

之前的持续动画问题,大概率是因为未正确截断路径或依赖了自动触发的状态更新。上面的实现中,percentage是纯计算属性,只有currentValue变化时才会更新位置,不会产生无意义的持续动画。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 21:37:40