Before we get into functionBuilder, let’s look at how to call a UIAlertController currently:

Let alertController = UIAlertController(title: "delete ", message:" Delete ") , preferredStyle:.alert) let deleteAction = UIAlertAction(preferredStyle:.alert) let deleteAction = UIAlertAction(preferredStyle:.alert) .destructive) {_ in // delete logic} let cancelAction = UIAlertAction(title: "unsubscribe ", style: .cancel) alertController.addAction(deleteAction) alertController.addAction(cancelAction)Copy the code

As you can see, this is quite a bit of code, so let’s see how you can use FunctionBuilder to build an API that is more comfortable to call.

What is a FunctionBuilder

This feature was introduced in Swift 5.1, and since it is not fully supported yet, we have to use the private @_functionBuilder modifier instead of @FunctionBuilder. You can find the official introduction here.

This feature focuses on representing HTML trees in DSLS, but it is also used extensively in SwiftUI, such as @ViewBuilder.

The following three methods are mainly used:

  • Must implement: buildBlock(_ components: Component…) -> Component
  • Optional: buildIf(_ component: component?) -> Component
  • Optional: buildEither(first: Component) -> Component/buildEither(second: Component) -> Component

At the beginning of FunctionBuilder experience

Now let’s get real. First declare an Action instead of UIAlertAction:

struct Action {
    let title: String
    let style: UIAlertAction.Style
    let action: () -> Void
}
Copy the code

Next declare a factory method to create UIAlertController objects:

func makeAlertController(title: String,
                         message: String,
                         style: UIAlertController.Style,
                         actions: [Action]) -> UIAlertController {
    let controller = UIAlertController(
        title: title,
        message: message,
        preferredStyle: style
    )
    for action in actions {
        let uiAction = UIAlertAction(title: action.title, style: action.style) { _ in
            action.action()
        }
        controller.addAction(uiAction)
    }
    return controller
}
Copy the code

Then, there is our highlight -functionBuilder use:

@_functionBuilder
struct ActionBuilder {
    typealias Component = [Action]
    static func buildBlock(_ children: Component...) -> Component {
        return children.flatMap { $0 }
    }
}
Copy the code

Use it to create UIAlertController:

func Alert(title: String,
               message: String,
               @ActionBuilder _ makeActions: () -> [Action]) -> UIAlertController {
        makeAlertController(title: title,
                            message: message,
                            style: .alert,
                            actions: makeActions())
    }
Copy the code

Ok, now it’s time to taste the fruits of victory. Let’s see how UIAlertController is now called:

Let alertVC = Alert(title: "delete ", message:" confirm delete?" ) {() - > [Action] in [the Action (the title: "delete", style: destructive, Action: {/ *... * /})] [the Action (the title: "cancel", style: .cancel, action: {})] }Copy the code

Er… While the code is indeed simplified, the closure [Action…] It still looks weird. This can be resolved by wrapping it as a class method, updating the Action code as follows:

extension Action {
    static func `default`(_ title: String, action: @escaping () -> Void) -> [Action] {
        return [Action(title: title, style: .default, action: action)]
    }

    static func destructive(_ title: String, action: @escaping () -> Void) -> [Action] {
        return [Action(title: title, style: .destructive, action: action)]
    }

    static func cancel(_ title: String, action: @escaping () -> Void = {}) -> [Action] {
        return [Action(title: title, style: .cancel, action: action)]
    }
}
Copy the code

Now let’s look at usage:

Let alertVC = Alert(title: "delete ", message:" confirm delete?" ) {() -> [Action] in action.destructive (" Delete ") {} action.cancel (" cancel ")}Copy the code

Well, it looks much better 😊.

Further – How to support if

If we wanted to add an if statement to the Alert closure, the compiler would raise an error: Closure containing Control flow statement cannot be used with function Builder’ ActionBuilder’. For example:

Let alertVC = Alert(title: "delete ", message:" confirm delete?" ) {() -> [Action] in action.destructive (" Delete ") {} if condition {} action.cancel (" cancel ")}Copy the code

This error can be made by implementing func buildIf(_ component: component?) The ActionBuilder code is updated as follows: -> Component

@_functionBuilder struct ActionBuilder { typealias Component = [Action] static func buildBlock(_ children: Component...) -> Component { return children.flatMap { $0 } } static func buildIf(_ component: Component?) -> Component { return component ?? []}}Copy the code

To support if-else, add the following two methods:

static func buildEither(first component: Component) -> Component {
    return component
}

static func buildEither(second component: Component) -> Component {
    return component
}
Copy the code

Note: Func buildIf(_ component: Component?) is no longer needed if the above two methods are added. – > Component.

Last attempt – support for-in

For example:

Let alertVC = Alert(title: "delete ", message:" confirm delete?" ) {() -> [Action] in for STR in [" delete ", "cancel "] {action.default (STR) {print(STR)}}}Copy the code

Create a helper function to solve:

func ForIn<S: Sequence>(
    _ sequence: S,
    @ActionBuilder makeActions: (S.Element) -> [Action]
) -> [Action] {

    return sequence
        .map(makeActions) // [[Action]]
        .flatMap { $0 }   // [Action]
}
Copy the code

Finally, use it like this:

Let alertVC = Alert(title: "delete ", message:" confirm delete?" ) {() -> [Action] in ForIn([" delete ", "cancel "]) {string in action.default (string) {print(string)}}}Copy the code

Finally, a code comparison:

Let alertController = UIAlertController(title: "delete ", message:" Delete ") , preferredStyle:.alert) let deleteAction = UIAlertAction(preferredStyle:.alert) let deleteAction = UIAlertAction(preferredStyle:.alert) .destructive) {_ in // delete logic} let cancelAction = UIAlertAction(title: "unsubscribe ", style: . Cancel) alertController. AddAction (deleteAction) alertController. AddAction (cancelAction) / / using the @ _functionBuilder after UIAlertController let alertVC = Alert(title: "delete ", message:" Confirm deletion?" ) {() -> [Action] in action.destructive (" Delete ") {} action.cancel (" cancel ")}Copy the code

👀 if you want to use @_functionBuilder, please don’t tell me.