【问题标题】:SwiftUI - Fit X Circles of varying size (wrt each other) in a given frame (Packed Bubble Chart)SwiftUI - 在给定的框架(打包气泡图)中适合不同大小(彼此)的 X 圈
【发布时间】:2023-01-05 02:55:03
【问题描述】:

有什么方法可以用 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)
    ]

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

我能想到的一种方法是在给定视图中使用相对于父级大小的 X Circles。但这本身就给定位和确保圆圈不相互接触或重叠带来了问题。

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

【问题讨论】:

    标签: ios swift swiftui sprite-kit


    【解决方案1】:

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

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

    框架只是为了适应父母的大小:

    你这样称呼它:

    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)
            
        }
    }
    

    这是代码:

    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
        }
        
    }
    

    【讨论】:

    • 呜啊啊啊!!!这真太了不起了!这肯定会出现在我的 SwiftUI 学习资料中……@ChrisR 非常感谢……也将从中学到很多东西。
    【解决方案2】:

    这很有趣,回到学校数学:) 这是我的结果。较小的圆圈都围绕第一个大圆圈对齐,我没有解决“歌剧”在您图像中的定位,这似乎是不小心的。

    这是代码: 这是一个起点,没有安全检查

    import SwiftUI
    
    struct DataItem: Identifiable {
        var id = UUID()
        var title: String
        var size: CGFloat
        var color: Color
        var offset = CGSize.zero
    }
    
    
    struct ContentView: View {
        
        // test 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: "opera", size: 25, color: .mint)
        ]
        
                
        var body: some View {
            
            ZStack {
                ForEach(data, id: .id) { item in
                    ZStack {
                        Circle()
                            .frame(width: CGFloat(item.size))
                            .foregroundColor(item.color)
                        Text(item.title)
                    }
                    .offset(item.offset)
                }
            }
            
            // calculate and set the offsets - could be done at other time or place in code
            .onAppear {
                data[0].offset = CGSize.zero
                data[1].offset = CGSize(width: (data[0].size + data[1].size) / 2, height: 0 )
                    
                var alpha = CGFloat.zero
                
                for i in 2..<data.count {
                                    
                    // sides of the triangle from circle center points
                    let c = (data[0].size + data[i-1].size) / 2
                    let b = (data[0].size + data[i].size) / 2
                    let a = (data[i-1].size + data[i].size) / 2
                    
                    alpha += calculateAlpha(a, b, c)
                                    
                    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 ) )
    
        }
        
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    【讨论】:

    • Woaaah..太棒了..!!添加了一些其他信息。使用此解决方案,自定义视图将内容溢出边界(偏移量),Plus 也需要固定大小。有什么方法可以让它依赖于父框架/大小并确保它不会溢出?无论如何.. Awesomeeeee回答老兄..谢谢..
    • 多么好的反应,看看它让我为你做了什么:)
    【解决方案3】:

    只需创建您的自定义形状,例如:

    struct Square: Shape {
        func path(in rect: CGRect) -> Path {
            let side = max(rect.width, rect.height)
            let top = rect.minY - (side - rect.height) / 2
            let left = rect.minX - (side - rect.width) / 2
            
            var path = Path()
            path.move(to: CGPoint(x: left, y: top))
            path.addLines([
                CGPoint(x: left+side, y: top),
                CGPoint(x: left+side, y: top+side),
                CGPoint(x: left, y: top+side),
                CGPoint(x: left, y: top)
            ])
            
            return path
        }
    }
    

    然后在您的文字上使用该形状:

    struct SquareButton: View {
        @State var geometry: GeometryProxy?
        
        var body: some View {
            Text("demo")
                .foregroundColor(Color.white)
                .background(
                    Square()
                )
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2021-11-08
      • 2019-10-15
      • 1970-01-01
      • 1970-01-01
      • 2011-12-23
      • 1970-01-01
      • 2019-04-10
      • 1970-01-01
      • 2018-06-13
      相关资源
      最近更新 更多