如何为 SwiftUI 定义 ShapeBuilder

use*_*ser 0 swift swiftui

正如我们所知,我们有一个非常有用的包装器,称为 @ViewBuilder 用于视图,但我缺少相同的 Shape 协议包装器,由于没有可用的包装器,我希望尝试构建一个用于学习多孔的包装器。

以下是视图的工作代码,并希望构建形状的包装器:

@ViewBuilder func testForView(value: Bool) -> some View {
    if value {
         Text("Hello")
    }
    else {
         Button("world!") {}
    }
}

// The Goal:
@ShapeBuilder func testForShape(value: Bool) -> some Shape {
    if value {
         Circle()
    }
    else {
         Rectangle()
    }
}
Run Code Online (Sandbox Code Playgroud)

我有使用包装器甚至定义自定义包装器的经验,但我所有的尝试都是包装一个值,而不是一个函数!我从来没有尝试过包装一个函数,坦白说我不知道​​它是 Swift 还是 SwiftUI 中的东西。那么我怎样才能使@ShapeBuilder像@ViewBuilder一样工作,我知道我必须使用Tuple或更正确的TupleShape,所以我认为我也应该定义TupleShape。

rob*_*off 8

首先我们应该问是否值得编写 aShapeBuilder而不是编写AnyShape这样的类型:

public struct AnyShape: Shape {
    public let makePath: (CGRect) -> Path

    public init(makePath: @escaping (CGRect) -> Path) {
        self.makePath = makePath
    }

    public init<Wrapped: Shape>(_ shape: Wrapped) {
        self.makePath = { shape.path(in: $0) }
    }

    public func path(in rect: CGRect) -> Path {
        return makePath(rect)
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以像这样AnyShape编写你的函数:testForShape

func testForShape(value: Bool) -> AnyShape {
    if value {
        return AnyShape(Circle())
    }
    else {
        return AnyShape(Rectangle())
    }
}
Run Code Online (Sandbox Code Playgroud)

AnyShape在各种项目中使用过。

更新

AppleAnyShape在 2022 年发布的 iOS 16、macOS 13 等版本中向 SwiftUI 添加了一个类型。Apple 版本没有makePath像我上面的实现那样具有初始化程序,但具有与初始化程序等效Wrapped初始化程序,这足以解决所述问题在问题中。

现在回到原来的

无论如何,如果我们想写ShapeBuilder,我们可以从最简单的实现开始,它只支持包含单个无条件子形状的主体:

@resultBuilder
public struct ShapeBuilder {
    public static func buildBlock<C0: Shape>(_ c0: C0) -> C0 { c0 }
}
Run Code Online (Sandbox Code Playgroud)

我们现在可以像这样使用它:

@ShapeBuilder
func shape1() -> some Shape {
    Circle()
}
Run Code Online (Sandbox Code Playgroud)

万岁!

我们可以通过添加零参数和返回类型来扩展它以支持包含子形状的主体:buildBlockEmptyShape

extension ShapeBuilder {
    public static func buildBlock() -> EmptyShape { EmptyShape() }
}

public struct EmptyShape: Shape {
    public func path(in rect: CGRect) -> Path { Path() }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以毫无错误地编写以下函数:

@ShapeBuilder
func shape0() -> some Shape {
}
Run Code Online (Sandbox Code Playgroud)

为了支持if不带子句的语句else,我们添加一个buildOptional方法和一个OptionalShape返回类型:

extension ShapeBuilder {
    public static func buildOptional<C0: Shape>(_ c0: C0?) -> OptionalShape<C0> {
        return OptionalShape(c0)
    }
}

public struct OptionalShape<Content: Shape>: Shape {
    public let content: Content?

    public init(_ content: Content?) {
        self.content = content
    }

    public func path(in rect: CGRect) -> Path {
        return content?.path(in: rect) ?? Path()
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以像这样使用它:

@ShapeBuilder
func shape2(flag: Bool) -> some Shape {
    if flag {
        Circle()
    }
}
Run Code Online (Sandbox Code Playgroud)

为了支持带有子句的if语句,并支持语句,我们添加和让它返回的类型:elseswitchbuildEitherEitherShape

extension ShapeBuilder {
    public static func buildEither<First: Shape, Second: Shape>(first: First) -> EitherShape<First, Second> {
        return .first(first)
    }

    public static func buildEither<First: Shape, Second: Shape>(second: Second) -> EitherShape<First, Second> {
        return .second(second)
    }
}

public enum EitherShape<First: Shape, Second: Shape>: Shape {
    case first(First)
    case second(Second)

    public func path(in rect: CGRect) -> Path {
        switch self {
        case .first(let first): return first.path(in: rect)
        case .second(let second): return second.path(in: rect)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们现在可以编写这个函数:

@ShapeBuilder
func shape3(_ n: Int) -> some Shape {
    if n < 0 {
        Rectangle()
    } else {
        switch n {
        case 0:
            EmptyShape()
        case 1:
            Rectangle()
        default:
            Capsule()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以通过编写一个buildBlock带有两个参数和一个Tuple2Shape返回值的方法来支持组合两个子形状:

extension ShapeBuilder {
    public static func buildBlock<C0: Shape, C1: Shape>(_ c0: C0, _ c1: C1) -> Tuple2Shape<C0, C1> {
        return Tuple2Shape(c0, c1)
    }
}

public struct Tuple2Shape<C0: Shape, C1: Shape>: Shape {
    public let tuple: (C0, C1)

    public init(_ c0: C0, _ c1: C1) {
        tuple = (c0, c1)
    }

    public func path(in rect: CGRect) -> Path {
        var path = tuple.0.path(in: rect)
        path.addPath(tuple.1.path(in: rect))
        return path
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以编写这个函数:

@ShapeBuilder
func shape4() -> some Shape {
    Circle()
    Rectangle()
}
Run Code Online (Sandbox Code Playgroud)

我们可以通过编写一个buildBlock带有三个参数和一个Tuple3Shape返回值的方法来支持组合两个子形状:

extension ShapeBuilder {
    public static func buildBlock<C0: Shape, C1: Shape, C2: Shape>(_ c0: C0, _ c1: C1, _ c2: C2) -> Tuple3Shape<C0, C1, C2> {
        return Tuple3Shape(c0, c1, c2)
    }
}

public struct Tuple3Shape<C0: Shape, C1: Shape, C2: Shape>: Shape {
    public let tuple: (C0, C1, C2)

    public init(_ c0: C0, _ c1: C1, _ c2: C2) {
        tuple = (c0, c1, c2)
    }

    public func path(in rect: CGRect) -> Path {
        var path = tuple.0.path(in: rect)
        path.addPath(tuple.1.path(in: rect))
        path.addPath(tuple.2.path(in: rect))
        return path
    }
}
Run Code Online (Sandbox Code Playgroud)

这让我们可以编写这个函数:

@ShapeBuilder
func shape5() -> some Shape {
    Circle()
    Rectangle()
    Capsule()
}
Run Code Online (Sandbox Code Playgroud)

ViewBuilder方法的数量最多buildBlock为 10,但我不会再写了。如果您需要的话,您可以自己做。

不管怎样,这里是ShapeBuilder为了方便复制和粘贴而实现的全部内容:

@resultBuilder
public struct ShapeBuilder {
    public static func buildBlock<C0: Shape>(_ c0: C0) -> C0 { c0 }
}

extension ShapeBuilder {
    public static func buildBlock() -> EmptyShape { EmptyShape() }
}

public struct EmptyShape: Shape {
    public func path(in rect: CGRect) -> Path { Path() }
}

extension ShapeBuilder {
    public static func buildOptional<C0: Shape>(_ c0: C0?) -> OptionalShape<C0> {
        return OptionalShape(c0)
    }
}

public struct OptionalShape<Content: Shape>: Shape {
    public let content: Content?

    public init(_ content: Content?) {
        self.content = content
    }

    public func path(in rect: CGRect) -> Path {
        return content?.path(in: rect) ?? Path()
    }
}

extension ShapeBuilder {
    public static func buildEither<First: Shape, Second: Shape>(first: First) -> EitherShape<First, Second> {
        return .first(first)
    }

    public static func buildEither<First: Shape, Second: Shape>(second: Second) -> EitherShape<First, Second> {
        return .second(second)
    }
}

public enum EitherShape<First: Shape, Second: Shape>: Shape {
    case first(First)
    case second(Second)

    public func path(in rect: CGRect) -> Path {
        switch self {
        case .first(let first): return first.path(in: rect)
        case .second(let second): return second.path(in: rect)
        }
    }
}

extension ShapeBuilder {
    public static func buildBlock<C0: Shape, C1: Shape>(_ c0: C0, _ c1: C1) -> Tuple2Shape<C0, C1> {
        return Tuple2Shape(c0, c1)
    }
}

public struct Tuple2Shape<C0: Shape, C1: Shape>: Shape {
    public let tuple: (C0, C1)

    public init(_ c0: C0, _ c1: C1) {
        tuple = (c0, c1)
    }

    public func path(in rect: CGRect) -> Path {
        var path = tuple.0.path(in: rect)
        path.addPath(tuple.1.path(in: rect))
        return path
    }
}

extension ShapeBuilder {
    public static func buildBlock<C0: Shape, C1: Shape, C2: Shape>(_ c0: C0, _ c1: C1, _ c2: C2) -> Tuple3Shape<C0, C1, C2> {
        return Tuple3Shape(c0, c1, c2)
    }
}

public struct Tuple3Shape<C0: Shape, C1: Shape, C2: Shape>: Shape {
    public let tuple: (C0, C1, C2)

    public init(_ c0: C0, _ c1: C1, _ c2: C2) {
        tuple = (c0, c1, c2)
    }

    public func path(in rect: CGRect) -> Path {
        var path = tuple.0.path(in: rect)
        path.addPath(tuple.1.path(in: rect))
        path.addPath(tuple.2.path(in: rect))
        return path
    }
}
Run Code Online (Sandbox Code Playgroud)