Swift如何让矢量图功能更简洁

How to make vector drawing functions more concise in Swift

我制作了一个按钮来匹配我的应用程序其余部分的样式,但是它太笨重了。我尝试了一些东西,但这是我所能得到的最小的东西。

class ButtonPath {
    private let wiggle: Int = 3
    func points(height: Int) ->
        ((Int,Int),(Int,Int),(Int,Int),
         (Int,Int),(Int,Int),(Int,Int),
         (Int,Int),(Int,Int),(Int,Int),
         (Int,Int),(Int,Int),(Int,Int))
    {
        func rand() -> Int { Int.random(in: -wiggle...wiggle) }
        func r2(x: Int) -> Int { Int.random(in: -x...x) }
        let screen: CGRect = UIScreen.main.bounds
        let widthMx = CGFloat(0.9)
        let origin = (x:15, y:15)
        let width = Int(screen.width * widthMx)

        // Corner points
        let tl   = (x: origin.x + rand(),   y: origin.x + rand()) // tl = Top Left, etc.
        let tr   = (x: origin.x + width + rand(),      y: origin.y + rand())
        let bl   = (x: origin.x + rand(),   y: origin.y + height + rand())
        let br   = (x: origin.x + width + rand(),      y: origin.y + height + rand())

        // Arc controls, we're drawing a rectangle counter-clockwise from the top left
        let a1c1 = (x: origin.x + rand(),   y: Int(Double(origin.y+height+rand()) * 0.3)) // a1c1 = Arc 1 Control 1
        let a1c2 = (x: origin.x + rand(),   y: Int(Double(origin.y+height+rand()) * 0.6))
        let a2c1 = (x: Int(Double(origin.x+width+rand()) * 0.3), y: origin.y + height + rand())
        let a2c2 = (x: Int(Double(origin.x+width+rand()) * 0.6), y: origin.y + height + rand())
        let a3c1 = (x: origin.x + width + rand(),                y: Int(Double(origin.y + height+rand()) * 0.6))
        let a3c2 = (x: origin.x + width + rand(),                y: Int(Double(origin.y + height+rand()) * 0.3))
        let a4c1 = (x: Int(Double(origin.x+width+rand()) * 0.6), y: origin.y + rand())
        let a4c2 = (x: Int(Double(origin.x+width+rand()) * 0.6), y: origin.y + rand())

        return (
            t1: tl, tr: tr, b1: bl, br: br,
            a1c1: a1c1, a1c2: a1c2, a2c1: a2c1,
            a2c2:a2c2, a3c1:a3c1, a3c2:a3c2, a4c1:a4c1, a4c2:a4c2
        )
    }
    func path (height:Int) -> Path {
        let (tl, tr, bl, br, a1c1, a1c2, a2c1, a2c2, a3c1, a3c2, a4c1, a4c2) = points(height: height)
        return Path { path in
            path.move( to: CGPoint(x: tl.0, y: tl.1) )
            path.addCurve( to: CGPoint(x: bl.0, y: bl.1), control1: CGPoint(x: a1c1.0, y: a1c1.1), control2: CGPoint(x: a1c2.0, y: a1c2.1))
            path.addCurve( to: CGPoint(x: br.0, y: br.1), control1: CGPoint(x: a2c1.0, y: a2c1.1), control2: CGPoint(x: a2c2.0, y: a2c2.1))
            path.addCurve( to: CGPoint(x: tr.0, y: tr.1), control1: CGPoint(x: a3c1.0, y: a3c1.1), control2: CGPoint(x: a3c2.0, y: a3c2.1))
            path.addCurve( to: CGPoint(x: tl.0-2, y: tl.1), control1: CGPoint(x: a4c1.0, y: a4c1.1), control2: CGPoint(x: a4c2.0, y: a4c2.1))
        }
    }
}

有没有我可以编写的函数来输出角、弧、路径等的值?

您可以通过多种方式简化代码:

  • 到处使用CGFloat而不是不断地在IntDouble之间转换。
  • 添加一些 fileprivate 运算符以在 CGPoint 上进行数学计算。如果您在其他文件中需要它们,或者在项目范围内添加它们。
  • 分解出一个摆动点的方法。
  • 分解出从当前点到另一点绘制波浪线的方法。

您显然使用的是 Path 类型。所以我推断你正在使用 SwiftUI。

SwiftUI 有一个您可能想要使用的 Shape 协议。您通过定义方法 path(in frame: CGRect) -> Path 来遵守 Shape 协议。由于 SwiftUI 为您提供了 Shape 的框架,因此您不必使用 GeometryReader 来获取形状的大小。

此外,由于 SwiftUI 可以随时向 View(包括 ShapePath)请求其 body,而且次数与它的次数一样多想要,使代码具有确定性很重要。这意味着不使用 SystemRandomNumberGenerator。所以让我们从定义一个确定性的 RandomNumberGenerator:

开始
struct Xorshift128Plus: RandomNumberGenerator {
    var seed: (UInt64, UInt64)

    mutating func next() -> UInt64 {
        // Based on 
        // The state must be seeded so that it is not everywhere zero.
        if seed == (0, 0) { seed = (0, 1) }
        var x = seed.0
        x ^= x << 23
        x ^= x >> 17
        x ^= seed.1
        x ^= seed.1 >> 26
        seed = (seed.1, x)
        return seed.0 &+ seed.1
    }
}

我们还需要这些算术运算符 CGPoint:

fileprivate func *(s: CGFloat, p: CGPoint) -> CGPoint { .init(x: s * p.x, y: s * p.y) }
fileprivate func +(p: CGPoint, q: CGPoint) -> CGPoint { .init(x: p.x + q.x, y: p.y + q.y) }
fileprivate func -(p: CGPoint, q: CGPoint) -> CGPoint { .init(x: p.x - q.x, y: p.y - q.y) }

现在我们准备好实现一个摆动点的方法:

extension CGPoint {
    fileprivate func wiggled<RNG: RandomNumberGenerator>(by wiggle: CGFloat, using rng: inout RNG) -> CGPoint {
        return .init(
            x: x + CGFloat.random(in: -wiggle...wiggle, using: &rng),
            y: y + CGFloat.random(in: -wiggle...wiggle, using: &rng))
    }
}

请注意,我们对 RandomNumberGenerator 类型是通用的,因为我们采用 inout。我们可以硬编码 Xorshift128PlusRandomNumberGenerator 的任何其他具体实现,而不是通用的。但为什么要限制自己?

有了点摆动器,我们可以在Path上实现一个addWigglyLine方法:

extension Path {
    fileprivate mutating func addWigglyLine<RNG: RandomNumberGenerator>(
        to p3: CGPoint, wiggle: CGFloat, rng: inout RNG)
    {
        let p0 = currentPoint ?? .zero
        let v = p3 - p0
        let p1 = (p0 + (1/3) * v).wiggled(by: wiggle, using: &rng)
        let p2 = (p0 + (2/3) * v).wiggled(by: wiggle, using: &rng)
        addCurve(to: p3, control1: p1, control2: p2)
    }
}

最后我们可以实现 WigglyRect,一个自定义的 Shape。同样,我们需要在 RandomNumberGenerator 上通用。请注意,由于我们需要两次提及 start/end 点,因此我们将其摆动存储在 p0 中,以便我们可以整齐地关闭路径。

struct WigglyRect<RNG: RandomNumberGenerator>: Shape {
    var rng: RNG
    var wiggle: CGFloat = 8

    func path(in frame: CGRect) -> Path {
        var rng = self.rng
        func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
            return CGPoint(x: x, y: y).wiggled(by: wiggle, using: &rng)
        }

        let rect = frame.insetBy(dx: wiggle, dy: wiggle)
        let (x0, x1, y0, y1) = (rect.minX, rect.maxX, rect.minY, rect.maxY)
        let p0 = p(x0, y0)
        var path = Path()
        path.move(to: p0)
        path.addWigglyLine(to: p(x1, y0), wiggle: wiggle, rng: &rng)
        path.addWigglyLine(to: p(x1, y1), wiggle: wiggle, rng: &rng)
        path.addWigglyLine(to: p(x0, y1), wiggle: wiggle, rng: &rng)
        path.addWigglyLine(to: p0, wiggle: wiggle, rng: &rng)
        path.closeSubpath()
        return path
    }
}

我在 macOS 上测试过。为了使用您的代码,我将您对 UIScreen.main.bounds 的使用更改为 CGRect(x: 0, y: 0, width: 300, height: 600)。这是预览代码:

struct ContentView: View {
    @State var seed: (UInt64, UInt64) = makeSeed()

    var body: some View {
        VStack(spacing: 20) {
            ButtonPath().path(height: 100)
                .stroke()
                .frame(height: 100)
                .background(Color.pink.opacity(0.2))
            WigglyRect(rng: Xorshift128Plus(seed: seed))
                .stroke()
                .frame(height: 100)
                .background(Color.pink.opacity(0.2))
            Spacer()
            Button("Reseed") {
                self.seed = Self.makeSeed()
            }
        } //
            .padding()
    }

    private static func makeSeed() -> (UInt64, UInt64) {
        (UInt64.random(in: .min ... .max), UInt64.random(in: .min ... .max))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(PreviewLayout.fixed(width: 300, height: 600))
    }
}

这是它的样子:

您的代码绘制了顶部形状。此答案中的代码绘制底部形状。我在每条路径后面放了粉红色的背景,以显示路径的框架。请注意,您的代码不会使路径符合其边界。