通过 functionBuilder 简化 UIAlertController 的调用

1,295 阅读2分钟

在开始介绍 functionBuilder 的用法前,我们先来看一下目前如何来调用一个 UIAlertController:

let alertController = UIAlertController(
    title: "删除",
    message: "确定删除?",
    preferredStyle: .alert
)
let deleteAction = UIAlertAction(title: "删除", style: .destructive) { _ in
    // 删除逻辑
}
let cancelAction = UIAlertAction(title: "取消", style: .cancel)
alertController.addAction(deleteAction)
alertController.addAction(cancelAction)

可以看到代码量还是比较多的,下面来看一下如何使用 FunctionBuilder ,来构建一个调用起来更加舒适的 API。

什么是 FunctionBuilder

该特性实在 Swift 5.1 开始提出的,由于它现在还未完全支持,所以我们只能使用私有的修饰符 @_functionBuilder,而不是 @functionBuilder。你可以在这里找到关于它官方的介绍。

该特性主要侧重于用 DSL 来表示HTML 树,但它也大量应用于 SwiftUI 中,比如 @ViewBuilder。

主要使用的是以下三个方法:

  • 必须实现: buildBlock(_ components: Component...) -> Component
  • 可选:buildIf(_ component: Component?) -> Component
  • 可选:buildEither(first: Component) -> Component / buildEither(second: Component) -> Component

FunctionBuilder 初体验

现在来具体实操一下,首先来声明一个 Action 来代替 UIAlertAction:

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

接下来声明一个工厂方法来创建 UIAlertController 对象:

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
}

然后,就是我们的重头戏-functionBuilder的使用:

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

使用它来创建 UIAlertController:

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

ok,现在到了品尝胜利果实的时候了,来看一下现在如何调用 UIAlertController:

let alertVC = Alert(title: "删除", message: "确认删除?") { () -> [Action] in
    [Action(title: "删除", style: .destructive, action: { /* ... */ })]
    [Action(title: "取消", style: .cancel, action: {})]
}

额....,虽然代码确实简化了,但闭包里的[Action...]看着还是很怪异。这个可以通过将其包装为类方法来解决,更新 Action 代码如下:

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)]
    }
}

现在再来看一下使用:

let alertVC = Alert(title: "删除", message: "确认删除?") { () -> [Action] in
    Action.destructive("删除") { }
    Action.cancel("取消")
}

嗯,看起来好多了😊。

更进一步 - 如何支持 if

假设我们想在 Alert 的闭包中添加 if 语句的话,编译器会报错:Closure containing control flow statement cannot be used with function builder 'ActionBuilder'。比如下面的代码:

let alertVC = Alert(title: "删除", message: "确认删除?") { () -> [Action] in
    Action.destructive("删除") { }
    if condition {  }
    Action.cancel("取消")
}

该错误可以通过实现 func buildIf(_ component: Component?) -> Component 来解决,更新后的 ActionBuilder 代码如下:

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

如需支持 if - else,则需添加下述两个方法:

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

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

Note:若添加上述两个方法,就不再需要 func buildIf(_ component: Component?) -> Component 了。

最后的尝试 - 支持 for - in

比如下述的代码:

let alertVC = Alert(title: "删除", message: "确认删除?") { () -> [Action] in
    for str in ["删除", "取消"] {
        Action.default(str) { print(str) }
    }
}

创建 helper 函数来解决:

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

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

最终,这样使用它:

let alertVC = Alert(title: "删除", message: "确认删除?") { () -> [Action] in
    ForIn(["删除", "取消"]) { string in
        Action.default(string) { print(string) }
    }
}

最后,来个代码对比:

let alertController = UIAlertController(
    title: "删除",
    message: "确定删除?",
    preferredStyle: .alert
)
let deleteAction = UIAlertAction(title: "删除", style: .destructive) { _ in
    // 删除逻辑
}
let cancelAction = UIAlertAction(title: "取消", style: .cancel)
alertController.addAction(deleteAction)
alertController.addAction(cancelAction)

// 使用 @_functionBuilder 后的 UIAlertController
let alertVC = Alert(title: "删除", message: "确认删除?") { () -> [Action] in
    Action.destructive("删除") { }
    Action.cancel("取消")
}

👀了上面的代码对比,要不要使用 @_functionBuilder 就不用我说了吧,大兄嘚。