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