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.