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 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 an AnyShape type-erased helper shape and erase the return maskingLayer to AnyShape. This is a similar approach to SwiftUI's built-in AnyView type-erasing View wrapper.

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 of solving this, is to define a result builder similar to SwiftUI's own @ViewBuilder. This library implements the @ShapeBuilder and @InsettableShapeBuilder result builders, allowing you to get rid of type-erasing shape wrappers and even return statements. Mark your computed property or functions with the according result builder name 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. Both protocols share similarities 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.

dependencies: [
  .package(
    url: "https://github.com/ohitsdaniel/ShapeBuilder.git",
    from: "0.1.0"
  )
],
targets: [
  .target(
    name: "MyAwesomePackage",
    dependencies: [
      "ShapeBuilder"
    ]
  )
]

Xcode

ShapeBuilder

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.

GitHub

https://github.com/ohitsdaniel/ShapeBuilder