SwiftUI - 在给定框架中适应不同大小的 X 个圆圈(彼此相连)(打包气泡图)

Kun*_*rma 0 ios sprite-kit swift swiftui

有没有办法用 SwiftUI 创建这样的东西(不使用 D3.js) -

// test data
    @State private var data: [DataItem] = [
        DataItem(title: "chrome", weight: 180, color: .green),
        DataItem(title: "firefox", weight: 60, color: .red),
        DataItem(title: "safari", weight: 90, color: .blue),
        DataItem(title: "edge", weight: 30, color: .orange),
        DataItem(title: "ie", weight: 50, color: .yellow),
        DataItem(title: "opera", weight: 25, color: .purple)
    ]
Run Code Online (Sandbox Code Playgroud)

在测试数据中,“权重”表示哪个项目应该更大/更小。

在此输入图像描述

我能想到的一种方法是在给定视图中设置 X 个圆圈,其大小相对于父级。但这本身就造成了定位和确保圆圈不会相互接触或重叠的问题。

不确定这里SpriteKit的用法?可以使用它还是可以仅使用 SwiftUI 组件来实现?

Chr*_*isR 5

好吧,你激励了我:)这是扩展版本

  • 独立结构
  • 适应父母!
  • 接受数据、间距、起始角度、方向(顺时针/逆时针)

框架只是为了显示适应父尺寸:

在此输入图像描述

你这样称呼它:

struct ContentView: View {
    
    // graph data
    @State private var data: [DataItem] = [
        DataItem(title: "chrome", size: 180, color: .green),
        DataItem(title: "firefox", size: 60, color: .red),
        DataItem(title: "safari", size: 90, color: .blue),
        DataItem(title: "edge", size: 30, color: .orange),
        DataItem(title: "ie", size: 50, color: .yellow),
        DataItem(title: "chrome", size: 120, color: .green),
        DataItem(title: "firefox", size: 60, color: .red),
        DataItem(title: "safari", size: 90, color: .blue),
        DataItem(title: "edge", size: 30, color: .orange),
        DataItem(title: "opera", size: 25, color: .mint)
    ]
    
    
    var body: some View {
        
        BubbleView(data: $data, spacing: 0, startAngle: 180, clockwise: true)
            .font(.caption)
            .frame(width: 300, height: 400)
            .border(Color.red)
        
    }
}
Run Code Online (Sandbox Code Playgroud)

这是代码:

struct DataItem: Identifiable {
    var id = UUID()
    var title: String
    var size: CGFloat
    var color: Color
    var offset = CGSize.zero
}

struct BubbleView: View {
    
    @Binding var data: [DataItem]
    
    // Spacing between bubbles
    var spacing: CGFloat
    
    // startAngle in degrees -360 to 360 from left horizontal
    var startAngle: Int
    
    // direction
    var clockwise: Bool
    
    struct ViewSize {
        var xMin: CGFloat = 0
        var xMax: CGFloat = 0
        var yMin: CGFloat = 0
        var yMax: CGFloat = 0
    }
    
    @State private var mySize = ViewSize()
    
    var body: some View {

        let xSize = (mySize.xMax - mySize.xMin) == 0 ? 1 : (mySize.xMax - mySize.xMin)
        let ySize = (mySize.yMax - mySize.yMin) == 0 ? 1 : (mySize.yMax - mySize.yMin)

        GeometryReader { geo in
            
            let xScale = geo.size.width / xSize
            let yScale = geo.size.height / ySize
            let scale = min(xScale, yScale)
                     
            ZStack {
                ForEach(data, id: \.id) { item in
                    ZStack {
                        Circle()
                            .frame(width: CGFloat(item.size) * scale,
                                   height: CGFloat(item.size) * scale)
                            .foregroundColor(item.color)
                        Text(item.title)
                    }
                    .offset(x: item.offset.width * scale, y: item.offset.height * scale)
                }
            }
            .offset(x: xOffset() * scale, y: yOffset() * scale)
        }
        .onAppear {
            setOffets()
            mySize = absoluteSize()
        }
    }
    
    
    // taken out of main for compiler complexity issue
    func xOffset() -> CGFloat {
        let size = data[0].size
        let xOffset = mySize.xMin + size / 2
        return -xOffset
    }
    
    func yOffset() -> CGFloat {
        let size = data[0].size
        let yOffset = mySize.yMin + size / 2
        return -yOffset
    }

    
    // calculate and set the offsets
    func setOffets() {
        if data.isEmpty { return }
        // first circle
        data[0].offset = CGSize.zero
        
        if data.count < 2 { return }
        // second circle
        let b = (data[0].size + data[1].size) / 2 + spacing
        
        // start Angle
        var alpha: CGFloat = CGFloat(startAngle) / 180 * CGFloat.pi
        
        data[1].offset = CGSize(width:  cos(alpha) * b,
                                height: sin(alpha) * b)
        
        // other circles
        for i in 2..<data.count {
            
            // sides of the triangle from circle center points
            let c = (data[0].size + data[i-1].size) / 2 + spacing
            let b = (data[0].size + data[i].size) / 2 + spacing
            let a = (data[i-1].size + data[i].size) / 2 + spacing
            
            alpha += calculateAlpha(a, b, c) * (clockwise ? 1 : -1)
            
            let x = cos(alpha) * b
            let y = sin(alpha) * b
            
            data[i].offset = CGSize(width: x, height: y )
        }
    }
    
    // Calculate alpha from sides - 1. Cosine theorem
    func calculateAlpha(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
        return acos(
            ( pow(a, 2) - pow(b, 2) - pow(c, 2) )
            /
            ( -2 * b * c ) )
        
    }
    
    // calculate max dimensions of offset view
    func absoluteSize() -> ViewSize {
        let radius = data[0].size / 2
        let initialSize = ViewSize(xMin: -radius, xMax: radius, yMin: -radius, yMax: radius)
        
        let maxSize = data.reduce(initialSize, { partialResult, item in
            let xMin = min(
                partialResult.xMin,
                item.offset.width - item.size / 2 - spacing
            )
            let xMax = max(
                partialResult.xMax,
                item.offset.width + item.size / 2 + spacing
            )
            let yMin = min(
                partialResult.yMin,
                item.offset.height - item.size / 2 - spacing
            )
            let yMax = max(
                partialResult.yMax,
                item.offset.height + item.size / 2 + spacing
            )
            return ViewSize(xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax)
        })
        return maxSize
    }
    
}
Run Code Online (Sandbox Code Playgroud)