作为一个严重依靠 SwiftUI 的开发者,同视图打交道是最往常不过的作业了。从第一次触摸 SwiftUI 的声明式编程办法开端,我便喜欢上了这种写代码的感觉。但触摸地越多,碰到的问题也越多。起先,我单纯地将很多问题称之为灵异现象,认为大概率是因为 SwiftUI 的不成熟导致的。跟着不断地学习和探究,发现其中有相当部分的问题仍是因为自己的认知不够所导致的,彻底可以改善或防止。

我将通过上下两篇博文,对构建 SwiftUI 视图的 ViewBuilder 进行探讨。上篇将介绍 ViewBuilder 背面的完结者 —— result builders ; 下篇将通过对 ViewBuilder 的仿制,进一步地探寻 SwiftUI 视图的隐秘。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

本文期望达到的方针

期望在阅览完两篇文章后能消除或减轻你对下列疑问的困惑:

  • 怎么让自界说视图、办法支撑 ViewBuilder
  • 为什么杂乱的 SwiftUI 视图简略在 Xcode 上卡死或呈现编译超时
  • 为什么会呈现 “Extra arguments” 的过错提示(仅能在同一层次放置有限数量的视图)
  • 为什么要慎重运用 AnyView
  • 怎么防止运用 AnyView
  • 为什么无论显示与否,视图都会包括一切挑选分支的类型信息
  • 为什么绝大多数的官方视图类型的 body 都是 Never
  • ViewModifier 同特定视图类型的 modifier 之间的区别

什么是 Result builders

介绍

result builders 答应某些函数通过一系列组件中隐式构建成果值,按照开发者设定的构建规则对组件进行摆放。通过对函数句子运用构建器进行转译,result builders 供给了在 Swift 中创立新的范畴特定言语( DSL )的才能(为了保存原始代码的动态语义,Swift 有意地约束了这些构建器的才能)。

与常见的运用点语法完结的类 DSL 相比,运用 result builders 创立的 DSL 运用更简略、无效内容更少、代码更简略了解(在表述具有挑选、循环等逻辑内容时尤为明显),例如:

运用点语法( Plot ):

.div(
    .div(
        .forEach(archiveItems.keys.sorted(by: >)) { absoluteMonth in
            .group(
                .ul(
                    .forEach(archiveItems[absoluteMonth]) { item in
                        .li(
                            .a(
                                .href(item.path),
                                .text(item.title)
                            )
                        )
                    }
                ),
                .if( show, 
                    .text("hello"), 
                  else: .text("wrold")
                 ),
            )
        }
    )
)

通过 result builders 创立的构建器 ( swift-html ):

Div {
    Div {
        for i in 0..<100 {
            Ul {
                for item in archiveItems[i] {
                    li {
                        A(item.title)
                            .href(item.path)
                    }
                }
            }
            if show {
                Text("hello")
            } else {
                Text("world")
            }
        }
    }
}

历史与发展

自 Swift 5.1 开端,result builders 便跟着 SwiftUI 的推出躲藏在 Swift 言语之中(其时名为 function builder)。跟着 Swift 与 SwiftUI 的不断进化,终究被正式纳入到 Swift 5.4 版别之中。现在苹果在 SwiftUI 结构中大量地运用了该功用,除了最常见的视图构建器(ViewBuilder)外,其他还包括:AccessibilityRotorContentBuilder、CommandsBuilder、LibraryContentBuilder、SceneBuilder、TableColumnBuilder、TableRowBuilder、ToolbarContentBuilder、WidgetBundleBuilder 等。别的,在最新的 Swift 提案中,已呈现了 Regex builder DSL 的身影。其他的开发者运用该功用也创立了不少的 第三方库。

基本用法

界说构建器类型

一个成果构建器类型有必要满意两个基本要求。

  • 它有必要通过@resultBuilder进行标示,这表明它打算作为一个成果构建器类型运用,并答应它作为一个自界说属性运用。

  • 它有必要至少完结一个名为buildBlock的类型办法

例如:

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.map{"⭐️" + $0 + ""}.joined(separator: " ")
    }
}

通过上面的代码,咱们便创立了一个具有最基本功用的成果构建器。运用办法如下:

@StringBuilder
func getStrings() -> String {
    "喜羊羊"
    "美羊羊"
    "灰太狼"
}
// ⭐️喜羊羊 ⭐️美羊羊 ⭐️灰太狼

为构建器类型供给满意的成果构建办法子集

  • buildBlock(_ components: Component...) -> Component

    用来构建句子块的组合成果。每个成果构建器至少要供给一个它的具体完结。

  • buildOptional(_ component: Component?) -> Component

    用于处理在特定履行中可能或不可能呈现的部分成果。当一个成果构建器供给了 buildOptional(_:) 时,转译后的函数可以运用没有 elseif 句子,一起也供给了对 if let 的支撑。

  • buildEither(first: Component) -> ComponentbuildEither(second: Component) -> Component

    用于在挑选句子的不同途径下树立部分成果。当一个成果构建器供给这两个办法的完结时,转译后的函数可以运用带有elseif句子以及 switch 句子。

  • buildArray(_ components: [Component]) -> Component

    用来从一个循环的一切迭代中收集的部分成果。在一个成果构建器供给了 buildArray(_:) 的完结后,转译后的函数可以运用 for...in 句子。

  • buildExpression(_ expression: Expression) -> Component

    它答应成果构建器区分表达式类型和组件类型,为句子表达式供给上下文类型信息。

  • buildFinalResult(_ component: Component) -> FinalResult

    用于对最外层的 buildBlock 成果的再包装。例如,让成果构建器躲藏一些它并不想对外的类型(转化成可对外的类型)。

  • buildLimitedAvailability(_ component: Component) -> Component

    用于将 buildBlock 在受限环境下(例如if #available)发生的部分成果转化为可合适任何环境的成果,以进步 API 的兼容性。

成果构建器选用 ad hoc 协议,这意味着咱们可以更灵敏的重载上述办法。然而,在某些状况下,成果构建器的转译进程会根据成果构建器类型是否完结了某个办法来改动其行为。

之后会通过示例对上述的办法逐一做详尽介绍。下文中,会将“成果构建器”简称为“构建器”。

典范一:AttributedStringBuilder

本例中,咱们将创立一个用于声明 AttributedString 的构建器。对 AttributedString 不太熟悉的朋友,可以阅览我的另一篇博文 AttributedString——不仅仅让文字更美丽。

典范一的完好代码可以在 此处 获取( Demo1 )

本例结束后,咱们将可以用如下的办法来声明 AttributedString :

@AttributedStringBuilder
var text: Text {
    "_*Hello*_"
        .localized()
        .color(.green)
    if case .male = gender {
        " Boy!"
            .color(.blue)
            .bold()
    } else {
        " Girl!"
            .color(.pink)
            .bold()
    }
}

ViewBuilder 研究(上)—— 掌握 Result builders

创立构建器类型

@resultBuilder
public enum AttributedStringBuilder {
    // 对应 block 中没有走 component 的状况
    public static func buildBlock() -> AttributedString {
        .init("")
    }
    // 对应 block 中有 n 个 component 的状况( n 为正整数 )
    public static func buildBlock(_ components: AttributedString...) -> AttributedString {
        components.reduce(into: AttributedString("")) { result, next in
            result.append(next)
        }
    }
}

咱们首要创立了一个名为 AttributedStringBuilder 的构建器,并为其完结了两个 buildBlock 办法。构建器在转译时会主动地挑选对应的办法。

现在,可以为 block 供给恣意数量的 component ( AttributedString ) ,buildBlock 会将其转化成指定的成果(AttributedString)。

在完结 buildBlock 办法时,components 与 block 的回来数据类型应根据实践需求界说,无需共同。

运用构建器转译 Block

可以选用显式的办法来调用构建器,例如:

@AttributedStringBuilder // 明确标示
var myFirstText: AttributedString {
    AttributedString("Hello")
    AttributedString("World")
}
// "HelloWorld"
@AttributedStringBuilder
func mySecondText() -> AttributedString {} // 空 block ,将调用 buildBlock() -> AttributedString
// ""

也可以选用隐式的办法调用构建器:

// 在 API 端标示
func generateText(@AttributedStringBuilder _ content: () -> AttributedString) -> Text {
    Text(content())
}
// 在客户端隐式调用
VStack {
    generateText {
        AttributedString("Hello")
        AttributedString(" World")
    }
}
struct MyTest {
    var content: AttributedString
    // 在结构办法中标示
    init(@AttributedStringBuilder _ content: () -> AttributedString) {
        self.content = content()
    }
}
// 隐式调用
let attributedString = MyTest {
    AttributedString("ABC")
    AttributedString("BBC")
}.content

无论以何种办法,假如在 block 的终究运用了 return 关键字来回来成果,则构建器将主动疏忽转译进程。例如:

@AttributedStringBuilder
var myFirstText: AttributedString {
    AttributedString("Hello") // 该句子将被疏忽
    return AttributedString("World") // 仅回来 World
}
// "World"

为了在 block 中运用构建器不支撑的语法,开发者会测验运用 return 来回来成果值,应防止呈现这种状况。因为这会导致开发者将失去通过构建器进行转译所带来的灵敏性。

下面的代码在运用构建器转译时和不运用构建器转译时的状况彻底不同:

// 构建器主动转译,block 只回来终究的合成成果,代码可正常履行
@ViewBuilder
func blockTest() -> some View {
    if name.isEmpty {
        Text("Hello anonymous!")
    } else {
        Rectangle()
            .overlay(Text("Hello \(name)"))
    }
}
// 构建器的转译行为因 return 而被疏忽。 block 中的挑选句子两个分支回来了两种不同的类型,无法满意有必要回来同一类型的要求(some View),编译无法通过。
@ViewBuilder
func blockTest() -> some View {
    if name.isEmpty {
        return Text("Hello anonymous!")
    } else {
        return Rectangle()
            .overlay(Text("Hello \(name)"))
    }
}

在 block 中运用如下的办法调用代码,可以在不影响构建器转译进程的状况下完结其他的作业:

@AttributedStringBuilder
var myFirstText: AttributedString {
    let _ = print("update") // 声明句子不会影响构建器的转译
    AttributedString("Hello") 
    AttributedString("World")
}

增加 modifier

在持续完善构建器其他的办法之前,咱们先为 AttributedStringBuilder 增加一些相似 SwiftUI 的 ViewModifier 功用,然后像 SwiftUI 那样便利的修正 AttributedString 的样式。增加下面的代码:

public extension AttributedString {
    func color(_ color: Color) -> AttributedString {
        then {
            $0.foregroundColor = color
        }
    }
    func bold() -> AttributedString {
        return then {
            if var inlinePresentationIntent = $0.inlinePresentationIntent {
                var container = AttributeContainer()
                inlinePresentationIntent.insert(.stronglyEmphasized)
                container.inlinePresentationIntent = inlinePresentationIntent
                let _ = $0.mergeAttributes(container)
            } else {
                $0.inlinePresentationIntent = .stronglyEmphasized
            }
        }
    }
    func italic() -> AttributedString {
        return then {
            if var inlinePresentationIntent = $0.inlinePresentationIntent {
                var container = AttributeContainer()
                inlinePresentationIntent.insert(.emphasized)
                container.inlinePresentationIntent = inlinePresentationIntent
                let _ = $0.mergeAttributes(container)
            } else {
                $0.inlinePresentationIntent = .emphasized
            }
        }
    }
    func then(_ perform: (inout Self) -> Void) -> Self {
        var result = self
        perform(&result)
        return result
    }
}

因为 AttributedString 是值类型,因此咱们需求创立一个新的仿制,并在其上修正属性。modifier 的运用办法如下:

@AttributedStringBuilder
var myFirstText: AttributedString {
    AttributedString("Hello")
         .color(.red)
    AttributedString("World")
         .color(.blue)
         .bold()
}

虽然只编写了很少的代码,但现在现已逐步有了点 DSL 的感觉了。

简化表达

因为 block 只能接收特定类型的 component ( AttributedString ),因此每行代码都需求增加 AttributedString 的类型前缀,导致作业量大,一起也影响了阅览体验。通过运用 buildExpression 可以简化这一进程。

增加下面的代码:

public static func buildExpression(_ string: String) -> AttributedString {
    AttributedString(string)
}

构建器会将 String 首要转化成 AttributedString,然后再将其传入到 buildBlock 中。增加上述代码后,咱们直接运用 String 替换掉 AttributedString:

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    "World"
}

不过,现在咱们又面临了新问题 —— 无法在 block 中混合运用 String 和 AttributedString。这是因为,假如咱们不供给自界说的 buildExpression 完结,构建器会通过 buildBlock揣度出 component 的类型是 AttributedString 。一旦咱们供给了自界说的 buildExpression ,构建器将不再运用主动揣度。处理的办法便是为 AttributedString 也创立一个 buildExpression :

public static func buildExpression(_ attributedString: AttributedString) -> AttributedString {
    attributedString
}

现在就可以在 block 中混用两者了。

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    AttributedString("World")
}

另一个问题是,咱们无法直接在 String 下运用咱们之前创立的 modifier 。因为之前的 modifier 是针对 AttributedString 的,点语法将只能运用针对 String 的办法。处理办法有两种:一是扩展 String ,将其转化成 AttributedString,二是为 String 增加上 modifier 转化器。咱们暂时先选用第二种较繁琐的办法:

public extension String {
    func color(_ color: Color) -> AttributedString {
        AttributedString(self)
            .color(color)
    }
    func bold() -> AttributedString {
        AttributedString(self)
            .bold()
    }
    func italic() -> AttributedString {
        AttributedString(self)
            .italic()
    }
}

现在,咱们现已可以快速、清晰地进行声明晰。

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
         .color(.red)
    "World"
         .color(.blue)
         .bold()
}

AttributedString 供给了对本地化字符串以及部分 Markdown 语法的支撑,但仅适用于通过 String.LocalizationValue 类型结构的 AttributedString ,可以通过如下的办法来处理这个问题:

public extension String {
    func localized() -> AttributedString {
        .init(localized: LocalizationValue(self))
    }
}

将字符串转化成选用 String.LocalizationValue 结构的 AttributedString,转化后将可直接运用为 AttributedString 编写的 modifier(你也可以对 String 选用相似的办法,然后防止为 String 重复编写 modifier)。

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
         .color(.red)
    "~**World**~"
         .localized()
         .color(.blue)
         //.bold()    通过 Markdown 语法来描述粗体。当时在运用 Markdown 语法的状况下,直接对 inlinePresentationIntent 进行设置会有冲突。
}

ViewBuilder 研究(上)—— 掌握 Result builders

构建器转译的逻辑

了解构建器是怎么转译的,将有助于之后的学习。

@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    AttributedString("World")
         .color(.red)
}

构建器在处理上面的代码时,将会转译成下面的代码:

var myFirstText: AttributedString {
    let _a = AttributedStringBuilder.buildExpression("Hello")  // 调用针对 String 的 buildExpression
    let _b = AttributedStringBuilder.buildExpression(AttributedString("World").color(.red)) // 调用针对 AtributedString 的 buildExpression
    return AttributedStringBuilder.buildBlock(_a,_b) // 调用支撑多参数的 buildBloack
}

上下两段代码彻底等价,Swift 会在幕后主动帮咱们完结了这个进程。

在学习创立构建器时,通过在构建器办法的完结内部增加打印指令,有助于更好地掌握每个办法的调用机遇。

增加挑选句子支撑( 不带 else 的 if )

result builders 在处理 包括不包括 else 的挑选句子时,选用了彻底不同的内部处理机制。关于不包括 elseif 只需求完结下面的办法即可:

public static func buildOptional(_ component: AttributedString?) -> AttributedString {
    component ?? .init("")
}

构建器在调用该办法时,将视条件是否达到传入不同的参数。条件未达到时,传入 nil 。运用办法为:

var show = true
@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    if show {
        "World"
    }
}

在增加了 buildOptional 的完结后,构建器也将一起支撑 if let 语法,例如:

var name:String? = "fat"
@AttributedStringBuilder
var myFirstText: AttributedString {
    "Hello"
    if let name = name {
        " \(name)"
    }
}

buildOptional 对应的转译代码为:

// 上面的 if 代码对应的逻辑
var myFirstText: AttributedString {
    let _a = AttributedStringBuilder.buildExpression("Hello")
    var vCase0: AttributedString?
    if show == true {
        vCase0 = AttributedStringBuilder.buildExpression("World")
    }
    let _b = AttributedStringBuilder.buildOptional(vCase0)
    return AttributedStringBuilder.buildBlock(_a, _b)
}
// 上面的 if let 代码对应的逻辑
var myFirstText: AttributedString {
    let _a = AttributedStringBuilder.buildExpression("Hello")
    var vCase0:AttributedString?
    if let name = name {
        vCase0 = AttributedStringBuilder.buildExpression(name)
    }
    let _b = AttributedStringBuilder.buildOptional(vCase0)
    return AttributedStringBuilder.buildBlock(_a,_b)
}

这便是为什么只需求完结 buildOptional 即可一起支撑 if ( 不包括 else ) 和 if let的原因。

增加对多分支挑选的支撑

关于 if else 以及 switch 语法,则需求完结 buildEither(first:)buildEither(second:) 两个办法:

// 对条件为真的分支调用 (左边分支)
public static func buildEither(first component: AttributedString) -> AttributedString {
    component
}
// 对条件为否的分支调用 (右侧分支)
public static func buildEither(second component: AttributedString) -> AttributedString {
    component
}

运用办法如下:

var show = true
@AttributedStringBuilder
var myFirstText: AttributedString {
    if show {
        "Hello"
    } else {
        "World"
    }
}

对应的转译代码为:

var myFirstText: AttributedString {
    let vMerged: AttributedString
    if show {
        vMerged = AttributedStringBuilder.buildEither(first: AttributedStringBuilder.buildExpression("Hello"))
    } else {
        vMerged = AttributedStringBuilder.buildEither(second: AttributedStringBuilder.buildExpression("World"))
    }
    return AttributedStringBuilder.buildBlock(vMerged)
}

在包括 else 句子时,构建器在转译时将发生一个二叉树,每个成果都被分配到其中的一个叶子上。关于在 if else 中呈现的不运用 else 的分支部分,构建器仍将通过 buildOptional 来处理,例如:

var show = true
var name = "fatbobman"
@AttributedStringBuilder
var myFirstText: Text {
    if show {
        "Hello"
    } else if name.count > 5 {
        name
    }
}

转译后的代码为:

// 转译后的代码
var myFirstText: AttributedString {
    let vMerged: AttributedString
    if show {
        vMerged = AttributedStringBuilder.buildEither(first: AttributedStringBuilder.buildExpression("Hello"))
    } else {
        // 首要运用 buildOptional 处理不包括 else 的状况
        var vCase0: AttributedString?
        if name.count > 5 {
            vCase0 = AttributedStringBuilder.buildExpression(name)
        }
        let _a = AttributedStringBuilder.buildOptional(vCase0)
        // 右侧分支终究汇总到 vMerged 上
        vMerged = AttributedStringBuilder.buildEither(second: _a)
    }
    return AttributedStringBuilder.buildBlock(vMerged)
}

关于 switch 的支撑也是选用同样的办法。构建器在转译时,将递归地运用上述规则。

或许大家会古怪, buildEither 的完结如此简略,并没有太大的含义。在 result builders 提案进程中也有不少人有这个疑问。其实 Swift 的这种规划有其相当合适的运用范畴。鄙人一篇【 仿制 ViewBuilder 】中,咱们将看到 ViewBuilder 是怎么通过 buildEither 来保存一切分支的类型信息。

支撑 for…in 循环

for...in 句子将一切迭代的成果一起收集到一个数组中,并传递给 buildArray。供给 buildArray 的完结即可让构建器支撑循环句子。

// 本例中,咱们将一切的迭代成果直接连接起来,生成一个 AttributedString
public static func buildArray(_ components: [AttributedString]) -> AttributedString {
    components.reduce(into: AttributedString("")) { result, next in
        result.append(next)
    }
}

运用办法:

@AttributedStringBuilder
func test(count: Int) -> Text {
    for i in 0..<count {
        " \(i) "
    }
}

对应的转译代码:

func test(count: Int) -> AttributedString {
    var vArray = [AttributedString]()
    for i in 0..<count {
        vArray.append(AttributedStringBuilder.buildExpression(" \(i)"))
    }
    let _a = AttributedStringBuilder.buildArray(vArray)
    return AttributedStringBuilder.buildBlock(_a)
}

进步版别兼容性

假如供给了 buildLimitedAvailability 的完结,构建器供给了对 API 可用性查看(如 if #available(..))的支撑。这种状况在 SwiftUI 中很常见,例如某些 View 或 modifier 仅支撑较新的渠道,咱们需求为不支撑的渠道供给其他的内容。

public static func buildLimitedAvailability(_ component: AttributedString) -> AttributedString {
    component
}

该办法并不会独立存在,它会和 buildOptionalbuildEither 一起运用。当 API 可用性查看满意条件后, result builders 会调用该完结。在 SwiftUI 中,为了固定类型,运用了 AnyView 对类型进行了抹除。

运用办法:

// 创立一个当时渠道不支撑的办法
@available(macOS 13.0, iOS 16.0,*)
public extension AttributedString {
    func futureMethod() -> AttributedString {
        self
    }
}
@AttributedStringBuilder
var text: AttributedString {
    if #available(macOS 13.0, iOS 16.0, *) {
        AttributedString("Hello macOS 13")
            .futureMethod()
    } else {
        AttributedString("Hi Monterey")
    }
}

对应的转译逻辑为:

var text: AttributedString {
    let vMerge: AttributedString
    if #available(macOS 13.0, iOS 16.0, *) {
        let _temp = AttributedStringBuilder
            .buildLimitedAvailability( // 对类型或办法进行抹除
                AttributedStringBuilder.buildExpression(AttributedString("Hello macOS 13").futureMethod())
            )
        vMerge = AttributedStringBuilder.buildEither(first: _temp)
    } else {
        let _temp = AttributedStringBuilder.buildExpression(AttributedString("Hi Monterey"))
        vMerge = AttributedStringBuilder.buildEither(second: _temp)
    }
    return = AttributedStringBuilder.buildBlock(vMerge)
}

对成果再包装

假如咱们供给了 buildFinalResult 的完结,构建器将在转译的终究,对成果运用 buildFinalResult 再度转化,并以 buildFinalResult 的回来值为终究的成果。

绝大多数状况下,咱们无需完结 buildFinalResult,构建器会将 buildBlock 的回来作为终究的成果。

public static func buildFinalResult(_ component: AttributedString) -> Text {
    Text(component)
}

为了演示,本例中咱们将 AttributedString 通过 buildFinalResult 转化为 Text ,运用办法:

@AttributedStringBuilder
var text: Text {  // 终究的成果类型已转译为 Text
    "Hello world"
}

对应的转译逻辑:

var text: Text {
    let _a = AttributedStringBuilder.buildExpression("Hello world")
    let _blockResult = AttributedStringBuilder.buildBlock(_a)
    return AttributedStringBuilder.buildFinalResult(_blockResult)
}

至此,咱们现已完结了本节开端设定的方针。不过当时的完结仍无法为咱们供给创立例如 SwiftUI 各种容器的可能性,这个问题将在典范二中得以处理。

典范二:AttributedTextBuilder

典范二的完好代码可以在 此处 获取( Demo2 )

版别一的缺少

  • 只能对 component(AttributedString、String)逐一增加 modifier,无法统一配置
  • 无法动态布局,buildBlock 将一切的内容连接起来,想换行也只能通过单独增加 \n 来完结

运用协议替代类型

上述问题发生的主要原因为:上面的 buildBlock 的 component 是特定的 AttributedString 类型,约束了咱们创立容器(其他的 component )的才能。可以参照 SwiftUI View 的方案来处理上述缺少,运用协议取代特定的类型,一起让 AttributedString 也契合该协议。

首要,咱们将创立一个新的协议 —— AttributedText :

public protocol AttributedText {
    var content: AttributedString { get }
    init(_ attributed: AttributedString)
}
extension AttributedString: AttributedText {
    public var content: AttributedString {
        self
    }
    public init(_ attributed: AttributedString) {
        self = attributed
    }
}

让 AttributedString 契合该协议:

extension AttributedString: AttributedText {
    public var content: AttributedString {
        self
    }
    public init(_ attributed: AttributedString) {
        self = attributed
    }
}

创立一个新的构建器 —— AttributedTextBuilder,它的最大改动便是将一切 component 的类型都改成了 AttributedText 。

@resultBuilder
public enum AttributedTextBuilder {
    public static func buildBlock() -> AttributedString {
        AttributedString("")
    }
    public static func buildBlock(_ components: AttributedText...) -> AttributedString {
        let result = components.map { $0.content }.reduce(into: AttributedString("")) { result, next in
            result.append(next)
        }
        return result.content
    }
    public static func buildExpression(_ attributedString: AttributedText) -> AttributedString {
        attributedString.content
    }
    public static func buildExpression(_ string: String) -> AttributedString {
        AttributedString(string)
    }
    public static func buildOptional(_ component: AttributedText?) -> AttributedString {
        component?.content ?? .init("")
    }
    public static func buildEither(first component: AttributedText) -> AttributedString {
        component.content
    }
    public static func buildEither(second component: AttributedText) -> AttributedString {
        component.content
    }
    public static func buildArray(_ components: [AttributedText]) -> AttributedString {
        let result = components.map { $0.content }.reduce(into: AttributedString("")) { result, next in
            result.append(next)
        }
        return result.content
    }
    public static func buildLimitedAvailability(_ component: AttributedText) -> AttributedString {
        .init("")
    }
}

为 AttributedText 创立 modifier :

public extension AttributedText {
    func transform(_ perform: (inout AttributedString) -> Void) -> Self {
        var attributedString = self.content
        perform(&attributedString)
        return Self(attributedString)
    }
    func color(_ color: Color) -> AttributedText {
        transform {
            $0 = $0.color(color)
        }
    }
    func bold() -> AttributedText {
        transform {
            $0 = $0.bold()
        }
    }
    func italic() -> AttributedText {
        transform {
            $0 = $0.italic()
        }
    }
}

至此咱们便具有了相似在 SwiftUI 中创立自界说视图控件的才能。

创立 Container

Container 相似 SwiftUI 中的 Group ,不改动布局,便利对 Container 内的元素统一设置 modifier。

public struct Container: AttributedText {
    public var content: AttributedString
    public init(_ attributed: AttributedString) {
        content = attributed
    }
    public init(@AttributedTextBuilder _ attributedText: () -> AttributedText) {
        self.content = attributedText().content
    }
}

因为 Container 也契合 AttributedText 协议,因此将被视为 component,并且可以对其运用 modifier 。运用办法:

@AttributedTextBuilder
var attributedText: AttributedText {
    Container {
        "Hello "
            .localized()
            .color(.red)
            .bold()
        "~World~"
            .localized()
    }
    .color(.green)
    .italic()
}

此时履行上面的代码,你会发现,原来红色的 Hello 也变成了绿色的,这与咱们预期的不一样。在 SwiftUI 中,内层的设定应优先于外层的设定。为了处理这个问题,咱们需求对 AttributedString 的 modifier 做一些修正。

public extension AttributedString {
    func color(_ color: Color) -> AttributedString {
        var container = AttributeContainer()
        container.foregroundColor = color
        return then {
            for run in $0.runs {
                $0[run.range].mergeAttributes(container, mergePolicy: .keepCurrent)
            }
        }
    }
    func bold() -> AttributedString {
        return then {
            for run in $0.runs {
                if var inlinePresentationIntent = run.inlinePresentationIntent {
                    var container = AttributeContainer()
                    inlinePresentationIntent.insert(.stronglyEmphasized)
                    container.inlinePresentationIntent = inlinePresentationIntent
                    let _ = $0[run.range].mergeAttributes(container)
                } else {
                    $0[run.range].inlinePresentationIntent = .stronglyEmphasized
                }
            }
        }
    }
    func italic() -> AttributedString {
        return then {
            for run in $0.runs {
                if var inlinePresentationIntent = run.inlinePresentationIntent {
                    var container = AttributeContainer()
                    inlinePresentationIntent.insert(.emphasized)
                    container.inlinePresentationIntent = inlinePresentationIntent
                    let _ = $0[run.range].mergeAttributes(container)
                } else {
                    $0[run.range].inlinePresentationIntent = .emphasized
                }
            }
        }
    }
    func then(_ perform: (inout Self) -> Void) -> Self {
        var result = self
        perform(&result)
        return result
    }
}

通过遍历 AttributedString 的 run 视图,咱们完结了同一属性的内层设定优先于外层设定。

创立 Paragraph

Paragraph 会在其中内容的首尾创立换行。

public struct Paragraph: AttributedText {
    public var content: AttributedString
    public init(_ attributed: AttributedString) {
        content = attributed
    }
    public init(@AttributedTextBuilder _ attributedText: () -> AttributedText) {
        self.content = "\n" + attributedText().content + "\n"
    }
}

通过将协议作为 component ,为构建器供给了更多的可能性。

Result builders 的改善与缺少

已完结的改善

从 Swift 5.1 开端,result builders 现已过几个版别的改善,增加了部分功用一起也处理了部分的性能问题:

  • 增加了buildOptional 并取消了 buildeIf,在保存了对 if (不包括 else )支撑的一起,增加了对 if let 的支撑
  • 从 SwiftUI 2.0 版别开端支撑了 switch 关键字
  • 修正了 Swift 5.1 版别的 buildBlock 的语法转译机制。制止了参数类型的“向后”传达。这是导致前期 SwiftUI 视图代码总呈现“ expression too complex to be solved in a reasonable time ” 编译过错的首要原因

当时的缺少

  • 短缺部分挑选和控制才能,如: guard 、break 、continue

  • 缺少将命名约束在构建器上下文内才能

    关于 DSL 来说,引进速记词是很常见的状况,当时为构建器创立 component ,只能选用创立新的数据类型(例如上文中的:Container、Paragraph )或大局函数的形式。期望将来可以让这些命名仅约束在上下文之内,不将其引进大局范围。

后续

Result builders 的基本功用非常简略,上文中,咱们仅有少量的代码是有关构建器办法的。但想创立一个好用、易用的 DSL 则需求付出巨大的作业量,开发者应根据自己的实践需求来衡量运用 result builders 的得失。

鄙人篇中,咱们将测验仿制一个与 ViewBuilder 基本形态共同的构建器,信任仿制的进程能让你对 ViewBuilder 以及 SwiftUI 视图有更深的了解和知道。

期望本文可以对你有所帮助。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】