SwiftUI交互式半环形进度条开发:滑块拖拽不顺畅问题求助
解决SwiftUI交互式半环形进度条拖拽不顺畅问题
我尝试用SwiftUI开发一款交互式半环形进度条,期望实现类似参考图的效果,但当前滑块拖拽不够顺畅,无法在半环形范围内平滑拖动以展示进度,现有代码如下:
import SwiftUI struct SemiCircularProgressBar: View { @State var progressValue: Float = 0.0 @State private var degrees: Double = -90 var body: some View { VStack { ZStack { ProgressBar(progress: self.$progressValue) .frame(width: 250.0, height: 250.0) .padding(40.0) ProgressThumb(progress: self.$progressValue) .frame(width: 30, height: 30) .offset(x: self.thumbOffset().x, y: self.thumbOffset().y) .rotationEffect(.degrees(54.5)) .gesture( DragGesture() .onChanged { gesture in let angle = self.angle(for: gesture.location) self.updateProgress(for: angle) } ) } Spacer() } } private func angle(for location: CGPoint) -> Double { let vector = CGVector(dx: location.x, dy: location.y) let angle = atan2(vector.dy, vector.dx) return Double(angle * 180 / .pi) } private func updateProgress(for angle: Double) { let totalAngle: Double = 220 let minAngle: Double = -110 let maxAngle: Double = minAngle + totalAngle var normalizedAngle = min(max(minAngle, angle), maxAngle) if normalizedAngle > 88.8 { normalizedAngle = 88.8 } let progress = Float((normalizedAngle - minAngle) / totalAngle) self.progressValue = progress self.degrees = normalizedAngle } private func thumbOffset() -> CGPoint { let thumbRadius: CGFloat = 125 // half of progress bar diameter let radians = CGFloat(degrees) * .pi / -100 let x = thumbRadius * cos(radians) let y = thumbRadius * sin(radians) return CGPoint(x: x, y: y) } } struct ProgressBar: View { @Binding var progress: Float var body: some View { ZStack { Circle() .trim(from: 0.3, to: 0.9) .stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)) .opacity(0.3) .foregroundColor(Color.gray) .rotationEffect(.degrees(54.5)) Circle() .trim(from: 0.3, to: CGFloat(self.progress)) .stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)) .fill(AngularGradient(gradient: Gradient(stops: [ .init(color: Color(hex: "ED4D4D"), location: 0.39000002), .init(color: Color(hex: "E59148"), location: 0.48000002), .init(color: Color(hex: "EFBF39"), location: 0.5999999), .init(color: Color(hex: "EEED56"), location: 0.7199998), .init(color: Color(hex: "32E1A0"), location: 0.8099997)]), center: .center)) .rotationEffect(.degrees(54.5)) VStack { Text("824").font(Font.system(size: 44)).bold().foregroundColor(Color(hex: "314058")) Text("Great Score!").bold().foregroundColor(Color(hex: "32E1A0")) } } } } struct ProgressThumb: View { @Binding var progress: Float var body: some View { Circle() .fill(Color.blue) .frame(width: 30, height: 30) } } extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let a, r, g, b: UInt64 switch hex.count { case 3: // RGB (12-bit) (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) case 6: // RGB (24-bit) (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) case 8: // ARGB (32-bit) (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: (a, r, g, b) = (1, 1, 1, 0) } self.init( .sRGB, red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, opacity: Double(a) / 255 ) } } #Preview { SemiCircularProgressBar() }
问题根源分析
- 坐标系统错误:拖拽手势的
location是相对于视图左上角的坐标,计算角度时未转换为以进度条中心为原点的坐标系,导致角度计算偏差。 - 弧度转换错误:
thumbOffset()中把角度转弧度时使用了/ -100的错误系数,应该用标准的/ 180转换,导致滑块位置偏移,拖拽时无法贴合进度条轨迹。 - 进度映射不匹配:进度条的
trim范围是0.3到0.9(对应220度半环),但原代码直接将progressValue作为trim的终点值,导致进度与半环范围不匹配。 - 多余的角度限制:
updateProgress()中额外限制normalizedAngle > 88.8属于冗余逻辑,已经通过min(max(...))限制了角度范围。
修复后的完整代码
import SwiftUI struct SemiCircularProgressBar: View { @State var progressValue: Float = 0.0 @State private var degrees: Double = -110 // 初始位置对应半环起点 var body: some View { VStack { ZStack(alignment: .center) { ProgressBar(progress: $progressValue) .frame(width: 250.0, height: 250.0) .padding(40.0) ProgressThumb() .frame(width: 30, height: 30) .offset(x: thumbOffset().x, y: thumbOffset().y) .gesture( DragGesture() .onChanged { gesture in // 将拖拽位置转换为以进度条中心为原点的坐标 let relativeLocation = CGPoint( x: gesture.location.x - 125 - 40, y: gesture.location.y - 125 - 40 ) let angle = angle(for: relativeLocation) updateProgress(for: angle) } ) } Spacer() } } private func angle(for location: CGPoint) -> Double { // 计算相对于中心的角度,转换为0-360度适配SwiftUI坐标系 let angle = atan2(location.y, location.x) * 180 / .pi return angle < 0 ? angle + 360 : angle } private func updateProgress(for angle: Double) { let totalAngle: Double = 220 let minAngle: Double = 250 // 对应原-110度(360-110) let maxAngle: Double = minAngle + totalAngle // 对应原110度 // 将角度限制在半环区间内 let normalizedAngle = min(max(minAngle, angle.truncatingRemainder(dividingBy: 360)), maxAngle) let progress = Float((normalizedAngle - minAngle) / totalAngle) progressValue = progress degrees = normalizedAngle - 360 // 转换回-110到110的范围 } private func thumbOffset() -> CGPoint { let thumbRadius: CGFloat = 125 // 进度条半径 let radians = CGFloat(degrees) * .pi / 180 // 正确的角度转弧度公式 let x = thumbRadius * cos(radians) let y = thumbRadius * sin(radians) return CGPoint(x: x, y: y) } } struct ProgressBar: View { @Binding var progress: Float var body: some View { ZStack { // 背景半环 Circle() .trim(from: 0.3, to: 0.9) .stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)) .opacity(0.3) .foregroundColor(Color.gray) .rotationEffect(.degrees(54.5)) // 进度半环:将0-1的progress映射到0.3-0.9的trim范围 Circle() .trim(from: 0.3, to: 0.3 + CGFloat(progress) * 0.6) .stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)) .fill(AngularGradient(gradient: Gradient(stops: [ .init(color: Color(hex: "ED4D4D"), location: 0.39), .init(color: Color(hex: "E59148"), location: 0.48), .init(color: Color(hex: "EFBF39"), location: 0.6), .init(color: Color(hex: "EEED56"), location: 0.72), .init(color: Color(hex: "32E1A0"), location: 0.81)]), center: .center)) .rotationEffect(.degrees(54.5)) VStack { // 根据进度动态计算分数 Text("\(Int(progress * 1000))") .font(Font.system(size: 44)).bold().foregroundColor(Color(hex: "314058")) // 根据进度动态切换提示文本 Text(progress > 0.8 ? "Great Score!" : "Keep Going!") .bold().foregroundColor(Color(hex: "32E1A0")) } } } } struct ProgressThumb: View { var body: some View { Circle() .fill(Color.blue) .shadow(radius: 3) } } extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let a, r, g, b: UInt64 switch hex.count { case 3: // RGB (12-bit) (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) case 6: // RGB (24-bit) (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) case 8: // ARGB (32-bit) (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: (a, r, g, b) = (1, 1, 1, 0) } self.init( .sRGB, red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, opacity: Double(a) / 255 ) } } #Preview { SemiCircularProgressBar() }
关键修改说明
- 坐标转换:将拖拽手势的位置转换为以进度条中心为原点的坐标,确保角度计算准确。
- 角度范围修正:将角度统一到0-360度范围处理,再映射回半环的-110到110度区间,避免角度跳转问题。
- 进度映射:将
progressValue(0-1)映射到进度条trim的0.3-0.9范围,确保进度与半环长度匹配。 - 动态内容优化:根据进度动态计算分数和提示文本,提升交互合理性。
- 冗余逻辑移除:删除
updateProgress()中多余的角度限制代码,简化逻辑。
内容的提问来源于stack exchange,提问作者Jayasabeen Appukuttan
相关产品推荐
相关产品推荐

