SwiftUI:如何为分段圆形的每个扇区添加对应标签
问题:SwiftUI分段圆形扇区标签定位失败
我是SwiftUI新手,开发带自定义Shape组件的应用,已经实现了可点击的分段圆形扇区按钮,但给每个扇区添加标签时,用.overlay和参考Stack Overflow的方案都没法让标签正确对应到所属扇区。
当前无标签状态实现代码
struct EmotionWheelView: View { // Prepare array of emotions for the selector let emotions: [Emotion] = Bundle.main.decode("emotions.json") // Bringing in the tapped and selected emotions to set in the parent @Binding public var tappedEmotion: String @Binding public var selectedEmotion: String @Binding public var selectedSegment: Int? var selectableEmotions: [Emotion] { var results: [Emotion] = [] if selectedEmotion.isEmpty { for emotion in emotions { if emotion.level == 1 { results.append(emotion) } } } else { var baseEmotion: Emotion { var result: Emotion = emotions[0] for emotion in emotions { if emotion.name == selectedEmotion { result = emotion } } return result } for emotion in emotions { for subID in baseEmotion.subID { if emotion.id == subID { results.append(emotion) } } } } return results } var data: [Double] { var results: [Double] = [] for _ in selectableEmotions { results.append(2.0) } return results } var colors: [Color] { var results: [Color] = [] var color: Color = .red for emotion in selectableEmotions { switch emotion.color { case "purple": color = Color.purple case "pink": color = Color.pink case "orange": color = Color.orange case "yellow": color = Color.yellow case "green": color = Color.green case "teal": color = Color.teal case "blue": color = Color.blue default: color = Color.black } results.append(color) } return results } var body: some View { EmotionWheel(selectableEmotions: selectableEmotions, data: data, colors: colors, tappedEmotion: $tappedEmotion, selectedSegment: $selectedSegment) .frame(maxHeight: 200) } } struct EmotionWheel: View { var selectableEmotions: [Emotion] var data: [Double] var colors: [Color] @Binding var tappedEmotion: String @Binding var selectedSegment: Int? private var totalValue: Double { data.reduce(0, +) } private var startAngles: [Double] { var angles: [Double] = [] var currentAngle = -Double.pi / 2 for value in data { angles.append(currentAngle) currentAngle += value / totalValue * 2 * .pi } return angles } private var endAngles: [Double] { var angles: [Double] = [] var currentAngle = -Double.pi / 2 for value in data { currentAngle += value / totalValue * 2 * .pi angles.append(currentAngle) } return angles } var body: some View { GeometryReader { geo in ZStack { ForEach(0..<data.count, id: \.self) { index in WheelSegment(startAngle: startAngles[index], endAngle: endAngles[index]) .stroke(Color(UIColor.systemBackground), lineWidth: 1) .fill(colors[index].gradient) .onTapGesture { withAnimation { selectedSegment = index tappedEmotion = selectableEmotions[index].name } } .scaleEffect(selectedSegment == index ? 1.1 : 1.0) .animation(.spring(dampingFraction: 0.8), value: selectedSegment) } Circle() .foregroundStyle(Color(UIColor.systemBackground)) .frame(width: geo.size.width / 2) .overlay { Text(tappedEmotion) .fontWeight(.bold) .foregroundStyle(getTappedColor(tappedEmotion: tappedEmotion)) .padding() } } .padding(40) } } } struct WheelSegment: Shape { var startAngle: Double var endAngle: Double func path(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) path.move(to: center) path.addArc(center: center, radius: radius, startAngle: Angle(radians: startAngle), endAngle: Angle(radians: endAngle), clockwise: false) path.closeSubpath() return path } } func getTappedColor(tappedEmotion: String) -> Color { let emotions: [Emotion] = Bundle.main.decode("emotions.json") var color: Color = .red var toColor: Emotion { for emotion in emotions { if emotion.name == tappedEmotion { return emotion } } return emotions[0] } switch toColor.color { case "purple": color = Color.purple case "pink": color = Color.pink case "orange": color = Color.orange case "yellow": color = Color.yellow case "green": color = Color.green case "teal": color = Color.teal case "blue": color = Color.blue default: color = Color.black } return color }
当前效果

预期效果(示意)

我试过用.overlay,也试过通过ZStack分别绘制颜色层和标签层,但均未成功。
解决方案:基于扇区中心角定位标签
核心思路是计算每个扇区的中心角度,通过三角函数计算标签的精准位置,让标签对应到扇区外侧。修改EmotionWheel结构体的代码如下:
struct EmotionWheel: View { var selectableEmotions: [Emotion] var data: [Double] var colors: [Color] @Binding var tappedEmotion: String @Binding var selectedSegment: Int? private var totalValue: Double { data.reduce(0, +) } private var startAngles: [Double] { var angles: [Double] = [] var currentAngle = -Double.pi / 2 for value in data { angles.append(currentAngle) currentAngle += value / totalValue * 2 * .pi } return angles } private var endAngles: [Double] { var angles: [Double] = [] var currentAngle = -Double.pi / 2 for value in data { currentAngle += value / totalValue * 2 * .pi angles.append(currentAngle) } return angles } // 新增:计算扇区的中心角度 private func centerAngle(for index: Int) -> Double { (startAngles[index] + endAngles[index]) / 2 } var body: some View { GeometryReader { geo in ZStack { // 扇区绘制层 ForEach(0..<data.count, id: \.self) { index in WheelSegment(startAngle: startAngles[index], endAngle: endAngles[index]) .stroke(Color(UIColor.systemBackground), lineWidth: 1) .fill(colors[index].gradient) .onTapGesture { withAnimation { selectedSegment = index tappedEmotion = selectableEmotions[index].name } } .scaleEffect(selectedSegment == index ? 1.1 : 1.0) .animation(.spring(dampingFraction: 0.8), value: selectedSegment) } // 中心圆形层 Circle() .foregroundStyle(Color(UIColor.systemBackground)) .frame(width: geo.size.width / 2) .overlay { Text(tappedEmotion) .fontWeight(.bold) .foregroundStyle(getTappedColor(tappedEmotion: tappedEmotion)) .padding() } // 新增:标签层 ForEach(0..<data.count, id: \.self) { index in let centerAngle = centerAngle(for: index) // 计算标签位置:基于中心角度,偏移到扇区外侧 let x = geo.size.width/2 + (geo.size.width/2 + 30) * cos(centerAngle) let y = geo.size.height/2 + (geo.size.height/2 + 30) * sin(centerAngle) Text(selectableEmotions[index].name) .font(.caption) .fontWeight(.medium) .position(x: x, y: y) // 调整文本方向,避免倒转 .rotationEffect(Angle(radians: centerAngle + .pi/2)) } } .padding(40) } } }
关键修改说明
- 计算中心角:新增
centerAngle(for:)方法,取扇区起始角和结束角的平均值,得到扇区的正中间方向 - 定位标签:用三角函数
cos和sin计算标签的position,+30是标签到轮盘边缘的距离,可根据需求调整 - 调整文本方向:添加
rotationEffect让文本顺着扇区方向显示,避免出现倒转的情况
这样修改后,标签就能精准对应到每个扇区的外侧位置,和预期效果一致。
内容的提问来源于stack exchange,提问作者Tabingtons
相关产品推荐
相关产品推荐

