A result builder implementation that allows to define shape building closures and variables.
Problem
In SwiftUI, you can end up in a situation in which you would like to change a Shape property based on a view style. Imagine, you build a view that should either take a circular appearance or have its corners rounded given a certain corner radius. Probably you would end up with something along the lines of:
struct MyFancyView: View {
let isRound: Bool
var body: some View {
// Fancy content here
.mask(maskingLayer)
}
var maskingLayer: some Shape {
if isRound {
return Circle()
} else {
return RoundedRectangle(cornerRadius: 10)
}
}
}
However, this code doesn’t compile because maskingLayer declares an opaque return type, but has no return statements in its body from which to infer an underlying type. In other words: Swift expects that maskingLayer is always of the same type.
Possible solutions
Type erasure
One way to solve this is to introduce a type-erased AnyShape helper and erase the returned maskingLayer to AnyShape. This approach is similar to SwiftUI’s built-in, type-erasing AnyView.
struct AnyShape: Shape {
let _path: (CGRect) -> Path
init<S: Shape>(_ shape: S) {
_path = shape.path(in:)
}
func path(in rect: CGRect) -> Path {
_path(rect)
}
}
struct MyFancyView: View {
let isRound: Bool
var body: some View {
// Fancy content here
.mask(maskingLayer)
}
var maskingLayer: some Shape {
if isRound {
return AnyShape(Circle())
} else {
return AnyShape(RoundedRectangle(cornerRadius: 10))
}
}
}
As you can see, this requires us to wrap our raw shapes in AnyShape type-erasing wrappers which isn’t the most beautiful code you’ll ever see, but it works, doesn’t it? 🤷♂️
Result builders to the rescue
A second approach to solving this, is to define a result builder similar to SwiftUI’s own @ViewBuilder. This library implements @ShapeBuilder and @InsettableShapeBuilder result builders, allowing you to get rid of type-erasing shape wrappers views and even return statements. Mark your computed property or functions with the according result builder and you’re good to go.
struct MyFancyView: View {
let isRound: Bool
var body: some View {
// Fancy content here
.mask(maskingLayer)
}
@ShapeBuilder var maskingLayer: some Shape {
if isRound {
Circle()
} else {
RoundedRectangle(cornerRadius: 10)
}
}
}
BuiltShape / BuiltInsettableShape
Additionally, this library provides the BuiltShape and BuiltInsettableShape protocols which shares similiarities with SwiftUI’s View protocol. They define a get-only shape computed property which is marked with a @ShapeBuilder/@InsettableShapeBuilder annotation.
This allows you to define BuiltShapes which themselves are shapes and take the form of the shape property.
struct MyFancyMask: BuiltShape {
let isCircle: Bool
var shape: some Shape {
if isCircle {
Circle()
} else {
RoundedRectangle(cornerRadius: 10)
}
}
}
Installation
Swift Package
If you want to add ShapeBuilder to your Swift packages, add it as a dependency to your Package.swift.
You can add ShapeBuilder to your project via Xcode. Open your project, click on File → Swift Packages → Add Package Dependency…, enter the repository url (https://github.com/ohitsdaniel/ShapeBuilder.git) and add the package products to your app target.
License
This library is released under the MIT license. See LICENSE for details.
ShapeBuilder
A result builder implementation that allows to define shape building closures and variables.
Problem
In SwiftUI, you can end up in a situation in which you would like to change a
Shapeproperty based on a view style. Imagine, you build a view that should either take a circular appearance or have its corners rounded given a certain corner radius. Probably you would end up with something along the lines of:However, this code doesn’t compile because maskingLayer declares an opaque return type, but has no return statements in its body from which to infer an underlying type. In other words: Swift expects that maskingLayer is always of the same type.
Possible solutions
Type erasure
One way to solve this is to introduce a type-erased AnyShape helper and erase the returned maskingLayer to AnyShape. This approach is similar to SwiftUI’s built-in, type-erasing AnyView.
As you can see, this requires us to wrap our raw shapes in AnyShape type-erasing wrappers which isn’t the most beautiful code you’ll ever see, but it works, doesn’t it? 🤷♂️
Result builders to the rescue
A second approach to solving this, is to define a result builder similar to SwiftUI’s own
@ViewBuilder. This library implements@ShapeBuilderand@InsettableShapeBuilderresult builders, allowing you to get rid of type-erasing shape wrappers views and even return statements. Mark your computed property or functions with the according result builder and you’re good to go.BuiltShape / BuiltInsettableShape
Additionally, this library provides the
BuiltShapeandBuiltInsettableShapeprotocols which shares similiarities with SwiftUI’sViewprotocol. They define a get-onlyshapecomputed property which is marked with a@ShapeBuilder/@InsettableShapeBuilderannotation.This allows you to define
BuiltShapes which themselves are shapes and take the form of the shape property.Installation
Swift Package
If you want to add ShapeBuilder to your Swift packages, add it as a dependency to your
Package.swift.Xcode
You can add ShapeBuilder to your project via Xcode. Open your project, click on File → Swift Packages → Add Package Dependency…, enter the repository url (https://github.com/ohitsdaniel/ShapeBuilder.git) and add the package products to your app target.
License
This library is released under the MIT license. See LICENSE for details.