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

WatchOS中SpriteKit复刻Apple Workout Rings的技术难题求助

我之前在做类似的WatchOS SpriteKit项目时也踩过这些坑,给你分享几个实用的解决方案:

一、解决SKShader导致线帽失效的问题

当你给SKShapeNode附加SKShader后,默认的lineCap和lineJoin设置会被Shader的自定义渲染覆盖——因为Shader直接操作片段着色器,不会继承ShapeNode的这些属性。这里有两个可行的思路:

1. 在Shader中手动实现圆角线帽

你可以在Shader里计算像素距离,给线条的两端和转角添加圆角效果。下面是一个针对环形进度的片段Shader示例,同时实现渐变和圆角:

void main() {
    // 获取当前点到圆心的相对坐标
    vec2 center = vec2(0.5, 0.5);
    float distanceFromCenter = length(v_tex_coord - center);
    
    // 定义环形的内外半径
    float innerRadius = 0.3;
    float outerRadius = 0.45;
    float halfLineWidth = (outerRadius - innerRadius) / 2.0;
    float midRadius = innerRadius + halfLineWidth;

    // 计算进度遮罩(u_progress为外部传入的0-1进度值)
    float angle = atan(v_tex_coord.y - center.y, v_tex_coord.x - center.x);
    float normalizedAngle = (angle + M_PI) / (2.0 * M_PI);
    float progressMask = step(normalizedAngle, u_progress);

    // 实现圆角线帽:判断像素到环形中心线的距离,平滑过渡
    float distanceToMidline = abs(distanceFromCenter - midRadius);
    float roundedMask = smoothstep(halfLineWidth, halfLineWidth - 0.02, distanceToMidline);

    // 组合渐变颜色(这里用角度控制从红到蓝的渐变)
    vec3 color = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), normalizedAngle);
    gl_FragColor = vec4(color, progressMask * roundedMask);
}

Swift侧代码配置:

let ringShader = SKShader(fileNamed: "RingShader.fsh")
ringShader.uniforms = [SKUniform(name: "u_progress", float: 0.0)]

let ringNode = SKShapeNode(ellipseOf: CGSize(width: 200, height: 200))
ringNode.fillColor = .clear
ringNode.strokeColor = .white // 颜色会被Shader覆盖,仅作占位
ringNode.lineWidth = 30
ringNode.shader = ringShader

这个方法完全用Shader控制渲染,同时实现渐变和圆角,不依赖ShapeNode的默认线帽属性。

2. 用SKCropNode结合双节点实现

如果你不想写复杂的Shader,可以用「基础圆角环+遮罩裁剪」的方式:

  • 第一个SKShapeNode:绘制完整的圆角环形(提前设置lineCap = .round),通过SKEffectNode添加渐变滤镜。
  • 第二个SKShapeNode:绘制扇形遮罩,用来裁剪出进度部分。
  • 将基础环放入SKCropNode,用扇形节点作为遮罩。

示例代码:

// 1. 创建带圆角的基础环形
let baseRing = SKShapeNode(ellipseOf: CGSize(width: 200, height: 200))
baseRing.fillColor = .clear
baseRing.strokeColor = .red
baseRing.lineWidth = 30
baseRing.lineCap = .round
baseRing.lineJoin = .round

// 2. 给基础环添加渐变效果
let gradientFilter = CIFilter(name: "CILinearGradient")!
gradientFilter.setValue(CIVector(x: 0, y: 0), forKey: "inputPoint0")
gradientFilter.setValue(CIVector(x: 200, y: 200), forKey: "inputPoint1")
gradientFilter.setValue(CIColor(red: 1, green: 0, blue: 0), forKey: "inputColor0")
gradientFilter.setValue(CIColor(red: 0, green: 1, blue: 0), forKey: "inputColor1")

let effectNode = SKEffectNode()
effectNode.filter = gradientFilter
effectNode.addChild(baseRing)

// 3. 创建进度遮罩(扇形路径)
let progress: CGFloat = 0.7
let path = UIBezierPath()
path.move(to: CGPoint(x: 100, y: 100))
path.addArc(withCenter: CGPoint(x: 100, y: 100), 
            radius: 115, 
            startAngle: -CGFloat.pi/2, 
            endAngle: -CGFloat.pi/2 + CGFloat.pi * 2 * progress, 
            clockwise: false)
path.close()

let maskNode = SKShapeNode(path: path.cgPath)
maskNode.fillColor = .white
maskNode.strokeColor = .clear

// 4. 裁剪出进度环
let cropNode = SKCropNode()
cropNode.maskNode = maskNode
cropNode.addChild(effectNode)

这个方法保留了ShapeNode的原生圆角线帽,同时通过滤镜实现渐变,适合不想深入Shader的场景。

二、实现重叠环效果(复刻Apple Workout Rings)

Apple的三个运动环是独立的环形节点,重叠部分通过渲染层级自动覆盖。你只需要给每个环设置不同的zPosition,按顺序添加到场景中即可:

// 封装创建环形节点的函数
func createRingNode(progress: CGFloat) -> SKNode {
    // 这里放入你前面实现的带渐变+圆角的环形节点逻辑
    // 返回对应的SKNode(比如CropNode或带Shader的ShapeNode)
}

// 创建三个环,设置不同层级
let moveRing = createRingNode(progress: 0.7)
moveRing.zPosition = 1
scene.addChild(moveRing)

let exerciseRing = createRingNode(progress: 0.5)
exerciseRing.zPosition = 2
scene.addChild(exerciseRing)

let standRing = createRingNode(progress: 0.9)
standRing.zPosition = 3
scene.addChild(standRing)

SpriteKit会按zPosition从低到高渲染,上层的环会自动覆盖下层的重叠区域,完美复刻官方效果。

三、替代方案:用UIKit/CoreAnimation实现

如果SpriteKit的Shader和ShapeNode限制太多,你也可以考虑用WatchOS支持的CAShapeLayer实现——CoreAnimation对环形进度和渐变的支持更成熟,无需处理SpriteKit的兼容问题:

// 创建环形Layer
let ringLayer = CAShapeLayer()
ringLayer.path = UIBezierPath(arcCenter: CGPoint(x: 100, y: 100), 
                              radius: 85, 
                              startAngle: -CGFloat.pi/2, 
                              endAngle: CGFloat.pi * 3/2, 
                              clockwise: true).cgPath
ringLayer.lineWidth = 30
ringLayer.lineCap = .round
ringLayer.fillColor = UIColor.clear.cgColor
ringLayer.strokeEnd = 0.8 // 设置进度

// 添加渐变效果
let gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
gradient.colors = [UIColor.red.cgColor, UIColor.orange.cgColor]
gradient.mask = ringLayer // 用环形Layer作为遮罩

// 添加到视图
let ringView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
ringView.layer.addSublayer(gradient)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 09:42:30