SwiftUI 在分段圆上标记每个线段

SwiftUI Label each segment on a segmented circle

提问人:Tabingtons 提问时间:11/16/2023 最后编辑:Benzy NeezTabingtons 更新时间:11/16/2023 访问量:35

问:

这里是新手,并决定使用 Shape 处理一个具有大元素的应用程序。我创建了一个视图,该视图绘制了一个圆的片段并将它们组合到该圆中。我现在尝试向每个片段添加文本以标记它们(因为它们充当按钮)。我尝试过使用简单的尝试,例如 .overlay,以及在论坛上搜索解决方案,例如: 如何使用 SwiftUI 在我的圈子片段中插入 UIImages?

不幸的是,我无法让标签坐在其各自的细分市场之上。我共享的代码删除了所有标记以显示其当前状态的尝试。

这是我绘制分段轮子的代码:

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
}

结果:

App showing the emotion wheel blank with no labels

目标的粗略表示(你明白了):

Same image as before but text laid over the segments showing the title

我尝试使用.overlay,我也尝试使用ZStack并制作两个“圆圈”,一个用于显示颜色,另一个用于显示标签。

SwiftUI 图形 叠加

评论


答:

0赞 Benzy Neez 11/16/2023 #1

我建议你用 a 来显示轮子的段和标签。ZStack

要使标签显示在圆圈周围的位置,但具有水平方向(如屏幕截图所示),您需要按特定顺序应用 3 个转换:

  • 负旋转效果,以否定下面文本的旋转
  • 偏移量,用于将标签从 的中心移出到沿圆周的某个位置ZStack
  • 正旋转效果,将偏移文本从 .ZStack

我发现调整您的代码以显示它的工作有点困难,因此这里有一个小的独立示例,仅显示各个位置的标签:

enum Emotion: CaseIterable {
    case happy
    case angry
    case disgusted
    case sad
    case delighted
    case muted
    case shocked

    var name: String {
        "\(self)".capitalized
    }
}

struct ContentView: View {
    let emotions: [Emotion] = Emotion.allCases
    let radius: CGFloat = 100

    func angleForIndex(index: Int) -> Double {
        let segmentAngle = 360 / Double(emotions.count)
        return (Double(index) + 0.5) * segmentAngle
    }

    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 10)
                .foregroundColor(.blue.opacity(0.3))
                .frame(width: radius * 2, height: radius * 2)
            ForEach(Array(emotions.enumerated()), id: \.offset) { index, emotion in
                Text(emotion.name)
                    .rotationEffect(.degrees(-angleForIndex(index: index)))
                    .offset(y: -radius)
                    .rotationEffect(.degrees(angleForIndex(index: index)))
            }
        }
    }
}

Screenshot

评论

0赞 Tabingtons 11/24/2023
太棒了,谢谢你在这方面的帮助。解决了我的问题,并给了我一些清理原始代码的技巧。奖金!