In my last post Property Wrappers with Combine I wrote about using Swift 5.1 ’s property wrappers to spice up some of my Combine code.

Since mixing up the latest Swift 5.1 hotnesss with Combine felt pretty good I thought this time I’d write about function builders. So let’s get started.

Swift Function Builders

Function Builders are a new feature in Swift 5.1 that allows you to abstract redundant functionality around executing chunks of code that produce similar results. 🤔

So far, most examples I’ve seen around function builders have been around building Domain Specific Languages, e.g. APIs to bring safety and power to writing HTML, CSS, or JSON in pure Swift like so:

func buildPage() -> String {
  return html {
    head {
      title("Page title")
      meta("Content-type", value: "utf8")
    }
    body {
      div { ... }
    }
  }
}

The trick about function builders is that they take a closure, evaluate each expression in that closure, and return a list of the resulting values. It’s as simple as that - but extremely powerful!

Let’s see a quick implementation of a function builder that takes a list of statements and builds a list of them to print on screen.

You start with defining the builder type:

@_functionBuilder
struct Concatenator {
  static func buildBlock(_ strings: String...) -> String {
    return strings.joined(separator: ", ")
  }
}

Note: Feature seems to still be wip - therefore the leading underscore in @_functionBuilder.

You define a new syntax sugar keyword in Swift called @Concatenator and you mark it be a function builder. The type that drives that new keyword is the Concatenator struct.

Concatenator features a single method of type buildBlock(T...) -> U e.g. it takes a list of values and returns some kind of result.

In the case of the code sample above Concatenator takes a list of strings and returns them joined as a list.

Now let’s make a function that uses @Concatenator:

func list(@Concatenator block: () -> String) {
  print(block())
}

list(block:) takes a list builder as a parameter and prints the result. Notice how the block parameter is marked with the @Concatenator keyword - that tells Swift that this closure parameter should be driven by the Concatenator struct you defined earlier.

And finally let’s see the example in practice:

func three() -> String { "three!" }

list {
  "one".uppercased()
  "two"
  three()
}

When you call list(block:) with a closure - each expression in the closure body will be evaluated and the list of all evaluation results will be fed to Concatenator.buildBlock(T...).

So each of “one”.uppercased(), “two”, and the result of calling three() will be given to Concatenator.buildBlock(T...) which will join them together and give the result back to list(block:) to print to the console:

ONE, two, three!

Okay, this is the shortest humanly possible introduction to function builders 😅 Now let’s try this with some Combine code.

Handling subscriptions with a function builder

Now, what kind of Combine related code you can think of in which each line returns “something” that you need to “handle” somehow? 🙄

Well, I hope you guessed it - when you’re creating series of subscriptions (like binding multiple model properties to various UI controls) you need to store each of your subscriptions.

So let’s very very quickly build some syntax to handle subscriptions en masse 🚚

First we’re going to start with defining a simple protocol that turns a class into a “type that manages subscriptions”. For example a ViewController or another kind of controller are all good candidates for adopting that protocol. Especially when dealing with subscriptions that drive UI it’s very handy to tie the subscriptions’ lifetimes to the lifetime of the UI’s controller.

The new protocol will simply require the class to feature a list of subscriptions:

protocol Subscribing: class {
  var subscriptions: [AnyCancellable] { get set }
}

Next, let’s move on to the function builder itself - it will simply evaluate any subscriptions you make in the closure and return a list of the resulting AnyCancellable values:

@_functionBuilder
struct Cancellables {
  static func buildBlock(_ elements: AnyCancellable...)
    -> [AnyCancellable] {
    return elements
  }
}

As said, @Cancellables is as simple as a function builder can get - it simply accepts a list of AnyCancellable values and returns the list back.

Using this new function builder I’m going to extend the Subscribing protocol and add two handy methods that will make use of @Cancellables like so:

extension Subscribing {
  func addSubscriptions(@Cancellables builder: () -> AnyCancellable) -> Void {
    subscriptions.append(builder())
  }
  
  func addSubscriptions(@Cancellables builder: () -> [AnyCancellable]) -> Void {
    subscriptions.append(contentsOf: builder())
  }
}

These two methods will take a list of expressions resulting in AnyCancellable and simply add the list to the type’s subscriptions property.

The two methods differ only in their return type - one takes care of the case where the consumer of the API provides only one expression in the closure and the other handles all other use cases.

Believe it or not - that’s all we’re gonna do today 🤓 Take a closure of expressions, fetch the results, add them to subscriptions. Ok, let’s try that in practice.

First, let’s start off with an empty controller:

class MyController: Subscribing {
  var subscriptions = [AnyCancellable]()
  
  func bindUI() {
  
  }
}

MyController is a Subscribing conforming type and it has an [AnyCancellable] property to store Combine subscriptions.

Now let’s add some Combine subscriptions via addSubscriptions which is automatically added to MyController via the Subscribing conformance:

addSubscriptions {
  Timer.publish(every: 0.1, on: RunLoop.main, in: .default)
    .autoconnect()
    .sink { _ in print(".", terminator: "") }

  Timer.publish(every: 0.3, on: RunLoop.main, in: .default)
    .autoconnect()
    .sink { _ in print("+", terminator: "") }

  Timer.publish(every: 1.0, on: RunLoop.main, in: .default)
    .autoconnect()
    .sink { _ in print("") }
}

I call addSubscriptions and in the closure create 3 timers via Combine. Notice how I didn’t need to store each of the subscriptions manually via store(in:).

What happens behind the stage is - the subscriptions are evaluated, the resulting cancellables are fed to @Cancellables, and finally Subscribing stores them in subscriptions.

Whenever MyController is released from memory, that will automatically cancel the subscriptions as well 🔥

And the 3 timers? They just print some ascii art to the console:

waterfall

😁

Where to go from here?

To learn more about the new Swift 5.1 syntax and especially using function builders to build UI with SwiftUI and Combine have a look at Combine: Asynchronous programming with Swift - this is where you can see all updates, discuss in the website forums, and more.