如何在SwiftUI中把这个Shape的平滑曲线边缘连接起来?

移动开发 2026-07-10

我有一个波浪形状的印章

struct WavyPostageStampShape: Shape {
    var amplitude: CGFloat = 8
    var wavesX: Int = 6
    var wavesY: Int = 10
    var phase: CGFloat = 0
    var step: CGFloat = 2

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let w = rect.width
        let h = rect.height

        func wave(_ t: CGFloat, waves: Int) -> CGFloat {
            let angle = phase + t * CGFloat(waves) * 2 * .pi
            return sin(angle)
        }

        // MARK: - TOP
        var topPoints: [CGPoint] = []
        for x in stride(from: rect.minX, through: rect.maxX, by: step) {
            let t = (x - rect.minX) / w
            let offset = wave(t, waves: wavesX) * amplitude
            topPoints.append(CGPoint(x: x, y: rect.minY - offset))
        }

        // MARK: - RIGHT
        var rightPoints: [CGPoint] = []
        for y in stride(from: rect.minY, through: rect.maxY, by: step) {
            let t = (y - rect.minY) / h
            let offset = wave(t, waves: wavesY) * amplitude
            rightPoints.append(CGPoint(x: rect.maxX + offset, y: y))
        }

        // MARK: - BOTTOM
        var bottomPoints: [CGPoint] = []
        for x in stride(from: rect.maxX, through: rect.minX, by: -step) {
            let t = (rect.maxX - x) / w
            let offset = wave(t, waves: wavesX) * amplitude
            bottomPoints.append(CGPoint(x: x, y: rect.maxY + offset))
        }

        // MARK: - LEFT
        var leftPoints: [CGPoint] = []
        for y in stride(from: rect.maxY, through: rect.minY, by: -step) {
            let t = (rect.maxY - y) / h
            let offset = wave(t, waves: wavesY) * amplitude
            leftPoints.append(CGPoint(x: rect.minX - offset, y: y))
        }

        // MARK: - PATH BUILDING

        guard let first = topPoints.first else { return path }
        path.move(to: first)

        // TOP
        for p in topPoints.dropFirst() {
            path.addLine(to: p)
        }

        // TOP-RIGHT corner (quad)
        if let lastTop = topPoints.last,
           let firstRight = rightPoints.first {
            let corner = CGPoint(x: rect.maxX, y: rect.minY)
            path.addQuadCurve(to: firstRight, control: corner)
        }

        // RIGHT
        for p in rightPoints.dropFirst() {
            path.addLine(to: p)
        }

        // RIGHT-BOTTOM corner
        if let lastRight = rightPoints.last,
           let firstBottom = bottomPoints.first {
            let corner = CGPoint(x: rect.maxX, y: rect.maxY)
            path.addQuadCurve(to: firstBottom, control: corner)
        }

        // BOTTOM
        for p in bottomPoints.dropFirst() {
            path.addLine(to: p)
        }

        // BOTTOM-LEFT corner
        if let lastBottom = bottomPoints.last,
           let firstLeft = leftPoints.first {
            let corner = CGPoint(x: rect.minX, y: rect.maxY)
            path.addQuadCurve(to: firstLeft, control: corner)
        }

        // LEFT
        for p in leftPoints.dropFirst() {
            path.addLine(to: p)
        }

        // LEFT-TOP corner
        if let lastLeft = leftPoints.last {
            let corner = CGPoint(x: rect.minX, y: rect.minY)
            path.addQuadCurve(to: first, control: corner)
        }

        path.closeSubpath()
        return path
    }
}

示例:

#Preview {
    WavyPostageStampShape(
        amplitude: 2,
        wavesX: 6,
        wavesY: 8,
        phase: Double.pi / 2
    )
    .stroke(Color.blue, lineWidth: 2)
    .frame(width: 120, height: 200)
}

结果:
在此输入图片描述

我想要这个:

在此输入图片描述

解决方案

我认为这个形状的主要问题是你没有为角落留出空间。

以顶部的波形为例,波形起点的x 坐标需要从 rect.minX 向内缩进,以给角落留出空间。

调整后的数值可以按如下方式计算:

let cornerRadius = 2 * amplitude
let adjustedMinX = rect.minX + cornerRadius
let adjustedMaxX = rect.maxX - cornerRadius
let adjustedMinY = rect.minY + cornerRadius
let adjustedMaxY = rect.maxY - cornerRadius

wh的计算需要相应更新:

let w = rect.width - (2 * cornerRadius)
let h = rect.height - (2 * cornerRadius)

顶部沿线波形的区块随之改为如下所示:

// MARK: - TOP
var topPoints: [CGPoint] = []
for x in stride(from: adjustedMinX, through: adjustedMaxX, by: step) {
    let t = (x - adjustedMinX) / w
    let offset = wave(t, waves: wavesX) * amplitude
    topPoints.append(CGPoint(x: x, y: rect.minY - offset))
}

等等。

这几乎解决了问题,但角落仍然不是很圆润。

调整角落的控制点会有帮助。例如:

// LEFT-TOP corner
let corner = CGPoint(x: rect.minX - (cornerRadius / 2), y: rect.minY - (cornerRadius / 2))
path.addQuadCurve(to: first, control: corner)

顺便说一句,你对可选项也有一些冗余的 if let 检查,导致了警告,这些检查可以直接删除。

这是应用了所有修改后的更新形状:

struct WavyPostageStampShape: Shape {
    var amplitude: CGFloat = 8
    var wavesX: Int = 6
    var wavesY: Int = 10
    var phase: CGFloat = 0
    var step: CGFloat = 2

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let cornerRadius = 2 * amplitude
        let adjustedMinX = rect.minX + cornerRadius
        let adjustedMaxX = rect.maxX - cornerRadius
        let adjustedMinY = rect.minY + cornerRadius
        let adjustedMaxY = rect.maxY - cornerRadius
        let w = rect.width - (2 * cornerRadius)
        let h = rect.height - (2 * cornerRadius)

        func wave(_ t: CGFloat, waves: Int) -> CGFloat {
            let angle = phase + t * CGFloat(waves) * 2 * .pi
            return sin(angle)
        }

        // MARK: - TOP
        var topPoints: [CGPoint] = []
        for x in stride(from: adjustedMinX, through: adjustedMaxX, by: step) {
            let t = (x - adjustedMinX) / w
            let offset = wave(t, waves: wavesX) * amplitude
            topPoints.append(CGPoint(x: x, y: rect.minY - offset))
        }

        // MARK: - RIGHT
        var rightPoints: [CGPoint] = []
        for y in stride(from: adjustedMinY, through: adjustedMaxY, by: step) {
            let t = (y - adjustedMinY) / h
            let offset = wave(t, waves: wavesY) * amplitude
            rightPoints.append(CGPoint(x: rect.maxX + offset, y: y))
        }

        // MARK: - BOTTOM
        var bottomPoints: [CGPoint] = []
        for x in stride(from: adjustedMaxX, through: adjustedMinX, by: -step) {
            let t = (adjustedMaxX - x) / w
            let offset = wave(t, waves: wavesX) * amplitude
            bottomPoints.append(CGPoint(x: x, y: rect.maxY + offset))
        }

        // MARK: - LEFT
        var leftPoints: [CGPoint] = []
        for y in stride(from: adjustedMaxY, through: adjustedMinY, by: -step) {
            let t = (adjustedMaxY - y) / h
            let offset = wave(t, waves: wavesY) * amplitude
            leftPoints.append(CGPoint(x: rect.minX - offset, y: y))
        }

        // MARK: - PATH BUILDING

        guard let first = topPoints.first else { return path }
        path.move(to: first)

        // TOP
        for p in topPoints.dropFirst() {
            path.addLine(to: p)
        }

        // TOP-RIGHT corner (quad)
        if let firstRight = rightPoints.first {
            let corner = CGPoint(x: rect.maxX + (cornerRadius / 2), y: rect.minY - (cornerRadius / 2))
            path.addQuadCurve(to: firstRight, control: corner)
        }

        // RIGHT
        for p in rightPoints.dropFirst() {
            path.addLine(to: p)
        }

        // RIGHT-BOTTOM corner
        if let firstBottom = bottomPoints.first {
            let corner = CGPoint(x: rect.maxX + (cornerRadius / 2), y: rect.maxY + (cornerRadius / 2))
            path.addQuadCurve(to: firstBottom, control: corner)
        }

        // BOTTOM
        for p in bottomPoints.dropFirst() {
            path.addLine(to: p)
        }

        // BOTTOM-LEFT corner
        if let firstLeft = leftPoints.first {
            let corner = CGPoint(x: rect.minX - (cornerRadius / 2), y: rect.maxY + (cornerRadius / 2))
            path.addQuadCurve(to: firstLeft, control: corner)
        }

        // LEFT
        for p in leftPoints.dropFirst() {
            path.addLine(to: p)
        }

        // LEFT-TOP corner
        let corner = CGPoint(x: rect.minX - (cornerRadius / 2), y: rect.minY - (cornerRadius / 2))
        path.addQuadCurve(to: first, control: corner)

        path.closeSubpath()
        return path
    }
}

演示示例现在看起来是这样的:

截图

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章