You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何用自定义形状创建六边形镶嵌(蜂窝网格)

使用SwiftUI的Path实现六边形镶嵌效果

我已经用Path创建了六边形Shape,但不知道怎么实现类似下图的镶嵌效果:

struct Hexagon: Shape {
  func path(in rect: CGRect) -> Path {
    var path = Path()
    let center = CGPoint(x: rect.midX, y: rect.midY)
    let size = min(rect.size.width, rect.size.height) / 2
    let angleOffset = CGFloat.pi / 6
    for i in 0..<6 {
      let angle = CGFloat(i) * CGFloat.pi / 3 + angleOffset
      let point = CGPoint(x: center.x + cos(angle) * size, y: center.y + sin(angle) * size)
      if i == 0 {
        path.move(to: point)
      } else {
        path.addLine(to: point)
      }
    }
    path.closeSubpath()
    return path
  }
}

期望实现的效果:
六边形镶嵌效果


实现方案

核心几何逻辑

六边形能无缝镶嵌的关键在于:

  • 六边形对边平行且长度相等,相邻行的六边形需要偏移半个宽度
  • 垂直方向上,相邻行的间距是六边形高度的3/4(上下行六边形会重叠1/4的高度)

方法一:用LazyVGrid快速实现

调整六边形的尺寸适配,配合行偏移即可快速得到镶嵌效果:

// 调整后的六边形Shape,保证宽高比符合镶嵌要求
struct Hexagon: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        // 以宽度为基准计算边长,确保六边形宽高比为2:√3
        let sideLength = rect.width / 2
        let size = sideLength
        
        let angleOffset = CGFloat.pi / 6
        for i in 0..<6 {
            let angle = CGFloat(i) * CGFloat.pi / 3 + angleOffset
            let point = CGPoint(x: center.x + cos(angle) * size, y: center.y + sin(angle) * size)
            if i == 0 {
                path.move(to: point)
            } else {
                path.addLine(to: point)
            }
        }
        path.closeSubpath()
        return path
    }
}

// 镶嵌视图
struct HexTessellationView: View {
    // 单个六边形的宽度
    private let hexWidth: CGFloat = 60
    // 自适应列数,根据屏幕宽度自动调整
    private let columns = [GridItem(.adaptive(minimum: 60))]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: hexWidth * sqrt(3)/2 - hexWidth/2) {
                ForEach(0..<60) { index in
                    Hexagon()
                        .fill(Color.blue.opacity(0.7))
                        .stroke(Color.black, lineWidth: 1)
                        .frame(width: hexWidth, height: hexWidth * sqrt(3))
                        // 奇数行的六边形偏移半个宽度
                        .offset(x: (index / columns.count) % 2 == 1 ? hexWidth/2 : 0)
                }
            }
            .padding()
        }
    }
}

方法二:自定义Layout实现精准控制

如果需要固定列数、动态调整位置等灵活需求,可使用自定义Layout:

struct HexLayout: Layout {
    // 固定列数,可按需修改
    let columnCount: Int
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard !subviews.isEmpty else { return .zero }
        let subviewSize = subviews[0].sizeThatFits(.unspecified)
        let hexWidth = subviewSize.width
        let hexHeight = subviewSize.height
        
        let rowCount = (subviews.count + columnCount - 1) / columnCount
        // 总宽度包含奇数行偏移的额外宽度
        let totalWidth = CGFloat(columnCount) * hexWidth + hexWidth/2
        // 总高度按行间距为3/4六边形高度计算
        let totalHeight = CGFloat(rowCount) * hexHeight * 0.75 + hexHeight * 0.25
        
        return CGSize(width: totalWidth, height: totalHeight)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        guard !subviews.isEmpty else { return }
        let subviewSize = subviews[0].sizeThatFits(.unspecified)
        let hexWidth = subviewSize.width
        let hexHeight = subviewSize.height
        
        var currentX = bounds.minX + hexWidth/2
        var currentY = bounds.minY + hexHeight/4
        
        for (index, subview) in subviews.enumerated() {
            let rowIndex = index / columnCount
            // 奇数行偏移半个宽度
            let xOffset = rowIndex % 2 == 1 ? hexWidth/2 : 0
            subview.place(at: CGPoint(x: currentX + xOffset, y: currentY), proposal: proposal)
            
            currentX += hexWidth
            // 换行时重置X坐标,调整Y坐标
            if (index + 1) % columnCount == 0 {
                currentX = bounds.minX + hexWidth/2
                currentY += hexHeight * 0.75
            }
        }
    }
}

// 使用自定义Layout的镶嵌视图
struct HexTessellationView: View {
    private let hexWidth: CGFloat = 60
    
    var body: some View {
        ScrollView {
            HexLayout(columnCount: 5) {
                ForEach(0..<50) { _ in
                    Hexagon()
                        .fill(Color.blue.opacity(0.7))
                        .stroke(Color.black, lineWidth: 1)
                        .frame(width: hexWidth, height: hexWidth * sqrt(3))
                }
            }
            .padding()
        }
    }
}

内容的提问来源于stack exchange,提问作者Yasha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 19:52:32