Home > Blockchain >  How can I define ShapeBuilder for SwiftUI
How can I define ShapeBuilder for SwiftUI

Time:02-12

As we know we have a very useful wrapper called @ViewBuilder for view but I am missing same wrapper for Shape protocol, Since there is no wrapper for this available, I am looking to try build one for learning porous.

Here is working code for view and looking to build the wrapper for 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()
    }
}

I have experience working with wrappers or even defining a custom wrapper, but all my attempts were wrapping a value, not a function! I never tried to wrapp a function, frankly I do not know that it is thing in Swift or SwiftUI. So how can I make @ShapeBuilder works like @ViewBuilder works, I know that I must use Tuple or more correctly TupleShape, so I think I should define TupleShape as well.

CodePudding user response:

First we should ask whether it's worth writing a ShapeBuilder instead of writing an AnyShape type like this:

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

We can use AnyShape to write your testForShape function like this:

func testForShape(value: Bool) -> AnyShape {
    if value {
        return AnyShape(Circle())
    }
    else {
        return AnyShape(Rectangle())
    }
}

I've used AnyShape in various projects.

Anyway, if we want to write ShapeBuilder, we can start with the simplest possible implementation, which only supports a body containing a single, unconditional child shape:

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

We can now use it like this:

@ShapeBuilder
func shape1() -> some Shape {
    Circle()
}

Hooray!

We can extend it to support a body containing no child shapes by adding a zero-argument buildBlock and an EmptyShape type for it to return:

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

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

Now we can write the following function without errors:

@ShapeBuilder
func shape0() -> some Shape {
}

To support an if statement without an else clause, we add a buildOptional method and an OptionalShape type for it to return:

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

Now we can use it like this:

@ShapeBuilder
func shape2(flag: Bool) -> some Shape {
    if flag {
        Circle()
    }
}

To support an if statement with an else clause, and to support a switch statement, we add buildEither and an EitherShape type for it to return:

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

We can now write this function:

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

We can support combining two child shapes by writing a buildBlock method that takes two arguments, and a Tuple2Shape for it to return:

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

Now we can write this function:

@ShapeBuilder
func shape4() -> some Shape {
    Circle()
    Rectangle()
}

We can support combining two child shapes by writing a buildBlock method that takes three arguments, and a Tuple3Shape for it to return:

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

That lets us write this function:

@ShapeBuilder
func shape5() -> some Shape {
    Circle()
    Rectangle()
    Capsule()
}

ViewBuilder has buildBlock methods up to arity 10, but I'm not going to write out any more. You can do it yourself if you need them.

Anyway, here's the ShapeBuilder implementation all together for easy copy'n'paste:

@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
    }
}
  • Related