起风了

书接上文,上篇文章中,咱们已经知道了@State 是特点包装器了,SwiftUI经过@State声明的特点来进行视图和数据的绑定。咱们写SwiftUI的代码的时分,常常会有如下相似代码:

struct SwiftUITest: View {
    @State var numbers: [Int] = []
    var body: some View {
        VStack {
            Text("标题").font(.largeTitle)
            Spacer().frame(height: 20)
            Text("内容")
        }
    }
}

咱们独自拎出VStack,看着它的代码不由得发生了两个疑问:

  • 这似乎是一个初始化办法,参数是一个尾随闭包,确认吗?
  • 初始化了不同的变量,但是都没有参数名,而且也无需运用标点符号分割,return的是三个的组合值吗?

所以接下来咱们经过Xcode进入到VStack 查看对外露出的API,至少一部分问题就能够恍然大悟了:

// 露出的API
@frozen public struct VStack<Content> : View where Content : View {
		@inlinable public init(alignment: HorizontalAlignment = .center, 
									spacing: CGFloat? = nil, 
									@ViewBuilder content: () -> Content)
		public typealias Body = Never
}
// 实践完成办法(来源WWDC21 Session 10253)
struct VStack<Content: View>: View {
...
		init(@ViewBuilder content: () -> Content) {
				self.content = content
		}
...
}

没错,确实是咱们了解的初始化办法,传入了一个闭包content用来初始化,能够看到关键在于闭包参数运用了@ViewBuilder 修饰符,看来对闭包中的不同特点进行兼并的操作是该修饰符的特性。所以咱们接下来持续去探究

清风袭来

那么什么是@ViewBuilder呢?咱们持续往下探究,在SwiftUI的源码中得到了它的API:

@resultBuilder public struct ViewBuilder {
			static func buildBlock  ->  EmptyView
			static func buildBlock<Content>(_ content: Content) -> Content where Content: View
}
...
extension ViewBuilder {
		public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
}
...

看来关键便是@resultBuilder了,同时咱们经过ViewBuilder 对外供给的办法,能够持续将VStack的初始化办法补全:

VStack.init(content: {
	Text("标题").font(.largeTitle)
   	Spacer().frame(height: 20)
	Text("内容")
	return // 此处必然运用了View Builder获取返回值
}

VStack.init(content: {
	let v0 = Text("标题").font(.largeTitle)
	let v1 = Spacer().frame(height: 20)
	let v2 = Text("内容")
	return ViewBuilder.buildBlock(v0, v1, v2)
})

ViewBuilder将创立的每一个元素的值都兼并成为了一个值并返回作为VStack中的内容。所以说最终它仍是转化为了咱们所了解的Swift的语法,这是合乎逻辑的,保证了统一性,而SwiftUI仅仅依据Swift的嵌入式DSL,这一点是要清晰的。回到ViewBuilder,它也仅仅经过@resultBuilder完成的一个特别类型,所以真正要弄明白的仍是resultBuilder成果生成器)

Result Builder的历史

Result Builder开始是在Swift的提案SE-0289 中提出的,是随着Swift5.4出来的,这个版别是它的第二版别,而在它的开始版别中,它的名字还不是Result Builder,而是Function Builder,所以现在去看关于SwiftUI方面的文章,相当一部分文章仍是运用Function Builder这个词。而Function Builder自Swift 5.1以来就一种是一个隐藏的特性,而运用它的最知名的Library,便是咱们一向提到的SwiftUI

看看SwiftUI通过什么来实现的:Result Builder

Result Builder的由来

作为iOS开发者运用UIKIt已经很久了,但是在处理杂乱UI的时分一向都是咱们的痛点,首要是布局杂乱,所以出现了一大批如SnapKit、PureLayOut等等这些优秀的布局结构,然后是用户交互或许数据改变的时分,又得管理多种状况的更新,又得手动去刷新对应的UI视图等等,总归是非常繁琐,而且还没法跨渠道,iOS上是UIKit,Mac OS上是AppKit,Watch OS上是WatchKit。

这个时分看着隔壁的Google掏出了Flutter这一更现代化的声明式UI结构,不仅是跨渠道的,Dart言语也使得它简略上手,假如你是Apple的开发者,一方面要处理历史遗留的问题,一方面需求对竞争对手做出回应,你会怎么做呢?

且不谈跨渠道,那需求操作系统底层的配合,单说开发一个更现代化的UI结构,处理这一类问题通常自界说一门编程言语也便是范畴特定言语(DSL)是更简略的,比方HTML & CSS来处理了Web界面的结构语义和样式的描绘。当然咱们也能够不去运用**DSL,**而是依据现有的封装语法封装一个声明式的UI结构,比方Facebook开源的ComponentKit,运用起来和下面很相似:

return body([
  division([
    	header1("Chapter 1. Loomings."),
    	paragraph(["Call me Ishmael. Some years ago"]),
    	paragraph(["There is now your insular city"])
  ]),
  division([
 		header1("Chapter 2. The Carpet-Bag."),
    	paragraph(["I stuffed a shirt or two"])
  ])
])

咱们能够看到它虽然运用各种辅佐函数供给了一种声明式的办法,但是实践上还有许多问题:

  • 这儿依然有许多标点符号:逗号、括号和方括号。虽然这个问题很简略,但是不可防止会给开发者带来困扰,最好是能够防止它。
  • 这儿为children运用了数组类型,而实践上类型挑选器要求它的元素具有一样的类型,虽然这个实例是OK的,但是这种情况是有限的,假如有其他不同的类型,那将造成很大的费事
  • 假如改变了上述的层级中的某个元素,比方动态展现文本,那事情又将会变得杂乱
division((useChapterTitles ? [header1("Chapter 1. Loomings.")] : []) +
    [paragraph(["Call me Ishmael. Some years ago"]),
     paragraph(["There is now your insular city"])])

……

简而言之,它依旧不是一个很现代化的UI结构,固然有它的特定,但是依然不如Flutter运用那么丝滑,由于上述问题它无法很好的处理这些问题,而实践上一个现代化的UI结构运用起来应该如下:

return body {
  let chapter = spellOutChapter ? "Chapter " : ""
  division {
    if useChapterTitles {
      	header1(chapter + "1. Loomings.")
   	 }
    paragraph {
      "Call me Ishmael. Some years ago"
    }
    paragraph {
      "There is now your insular city"
    }
  }
  division {
    if useChapterTitles {
      header1(chapter + "2. The Carpet-Bag.")
    }
    paragraph {
      "I stuffed a shirt or two"
    }
  }
}

上述的这种完成假如创立一个传统的DSL,那咱们需求从头完成一套新的语法,需求重写编译器来解析语法树,同时现有的Swift开发者必然会感到困惑,由于这不契合Swift用户的期望,相当于一门要去把握一门新的言语了(从Swift1~Swift5咱们已经把握了好多门新言语了),所以嵌入式DSLDSL详文下篇描绘)便是一个必然的挑选了,也便是将上述类型的完成以某种形式嵌入到咱们的Swift中。

嵌入式的DSL运用了宿主言语的笼统能力,并且省去了杂乱语法分析器(Parser)的进程,不需求从头完成模块、变量等特性。而在0289提案中,Apple这样描绘Swift:

Swift is designed with rich affordances for building expressive, type-safe interfaces for libraries. In some cases, a library’s interface is distinct enough and rich enough to form its own miniature language within Swift. We refer to this as aDomain Specific Language (DSL), because it lets you better describe solutions within a particular problem domain.

Swift在规划之初,就让它有足够的能力为Library规划富有表现力的、类型安全的接口,足以在Swift中构成自己的微型言语(DSL),也便是依据Swift的嵌入式DSL。而为了完成这个嵌入式的DSL,并处理上述的那些问题,Apple发布了ResultBuilder提案。

Result Builder的界说

那说来说去,什么是Result Builder呢?

This proposal describesresult builders, a new feature which allows certain functions (specially-annotated, often via context) to implicitly build up a result value from a sequence of components. (它允许某些函数从一系列组件中隐式的创立成果)

基本的思维便是将该办法中不同句子的成果运用一个builder type组合起来,如下:

// 初始源码
@TupleBuilder
func build() -> (Int, Int, Int) {
	1
	2
	3
}
// 实践上会转化为如下代码
func build() -> (Int, Int, Int) {
	let _a = TupleBuilder.buildExpression(1)
	let _b = TupleBuilder.buildExpression(2)
	let _c = TupleBuilder.buildExpression(3)
	return TupleBuilder.buildBlock(_a, _b, _c)
}

Result Builder效果于特定类型的接口,该类接口触及列表和树结构的声明,所以在许多的问题范畴(problem domains)它都很有用,比方生成结构化的数据(如XML或许JSON),比方视图层级(如SwiftUI)。

Result Builder的运用

运用Result Builder,首要的便是去创立一个Result Builder类型(相似上述的ViewBuilder),它需求满意两个基本的要求:

  • 它有必要运用 @resultBuilder 进行注解
  • 它有必要至少供给一种静态的buildBlock的成果办法

一旦成功创立一个Result Builder类型之后,能够在两种不同位置的地方运用该注解:

一、第一种是funcvarsubscript的声明上。

而对于var以及subscript 必需求界说一个getter办法,该注解其实便是直接效果在get办法上的,同样在func的完成时增加注解,是表明该注解直接效果在此办法上,实例如下:

class BBB {
		// 1、效果在var特点上
    @NSAttributedStringBuilder var text: NSAttributedString {
        get {
            Atext("--").foregroundColor(Color.red)
            Atext("hah")
        }
    }
		// 2、直接效果在办法上
    @NSAttributedStringBuilder func aaa() -> NSAttributedString {
        Atext("----").foregroundColor(Color.red)
        Atext("hah")
    }
		// 3、直接效果鄙人标上
    @NSAttributedStringBuilder subscript(index: Int) -> NSAttributedString {
        get {
 			Atext("haah")
 			Atext("hah")
        }
    }
}

这儿我运用了ResultBuilder 提案中引荐的案例NSAttributedStringBuilder 来做演示,上述案例中Atext能够理解为一个NSAttributedString,当该注解@NSAttributedStringBuilder效果到以上三者上时,其实代表的是效果到三者对应的办法上,将办法体中的每一句描绘对应的成果都组合起来。

二、效果在办法的参数上。

但是在测验的时分,编译器会给出清晰的提示:

Result builder attribute ‘NSAttributedStringBuilder’ can only be applied to a parameter of function type.

也便是说ResultBuilder的注解只能用于函数类型的参数上!在Swift中通常咱们运用的都是闭包。

// 效果在函数类型的办法参数上
public extension NSAttributedString {
    @discardableResult
    convenience init(@NSAttributedStringBuilder _ builer: () -> NSAttributedString) {
        self.init(attributedString: builer())
    }
}

那么怎么完成这个Result Builder呢?当咱们创立一个Result Builder 类型的时分,咱们其实仅仅创立了一个静态办法的容器。而这些静态办法便是效果于注解的办法体中的句子的,所以首要就需求看看,Result Builder 类型的静态办法有哪些?

  • buildBlock(_ components: Component...) -> Component 这是每一个ResultBuilder都有必要包括一个静态办法,担任将办法中的句子块成果组合起来。
  • buildOptional(_ component: Component?) -> Component 用来在一个或许存在也或许不存在的成果时,当该静态办法的容器供给该函数时,被效果的办法中的句子能够运用包括if不包括else的挑选句子
  • buildEither(first: Component) → Component 以及 buildEither(second: Component) → Component 用于在挑选句子从不同途径发生不同成果时,当该静态办法的容器供给该函数时,被效果的办法中的句子就能够运用if-else句子,以及switch句子
  • buildArray(_ components: [Component]) → Component 用于在循环中发生成果,当该静态办法的容器供给该函数时,被效果的办法中的句子能够运用for…in句子
  • buildExpression(_ expression: Expression) -> Component 用于效果于办法中的句子,将效果后的返回值作为buildBlock 办法的参数。
  • buildLimitedAvailability(_ component: Component) -> Component 效果于有限可用性上下文。比方if #available
  • buildFinalResult(_ component: Component) -> FinalResult 效果于buildBlock顶层函数体最外层调用所发生的成果。

Result Builder的完成

上面仅仅简略的介绍各个办法的含义,但是实践怎么运用仍是让人心生疑问,所以接下来我会以一个实例下手,咱们运用一个提案上的案例NSAttributedStringBuilder 来自界说一个简易描绘NSAttributedString的DSL。

首要咱们需求界说一个Component协议,用来声明字符串和字符特点,并将它们转化为富文本。

typealias Font = UIFont
typealias Color = UIColor
typealias Attributes = [NSAttributedString.Key: Any]
protocol Component {
    var string: String { get }
    var attributes: Attributes { get }
    var attributedString: NSAttributedString { get }
}
extension Component {
    var attributedString: NSAttributedString {
        return NSAttributedString.init(string: string, attributes: attributes)
    }
}
// 创立一个继承该协议的结构体
// 实例为NSAttributed
extension NSAttributedString {
    struct AttrText: Component {
        let string: String
        let attributes: Attributes
        init(_ string: String, attributes: Attributes = [:]) {
            self.string = string
            self.attributes = attributes
        }
    }
}
// 增加一点简略的增加特点的办法
typealias Atext = NSAttributedString.AttrText
extension Component {
    func addAttributes(_ newAttributes: Attributes) -> Component {
        var attributes = self.attributes
        for attribute in newAttributes {
            attributes[attribute.key] = attribute.value
        }
        return Atext(string, attributes: attributes)
    }
    func foregroundColor(_ color: Color) -> Component {
        addAttributes([.foregroundColor: color])
    }
    func font(_ font: Font) -> Component {
        addAttributes([.font: font])
    }
 }

根底版别

NSAttributedString{
    Atext("老子").foregroundColor(UIColor.red)
		Atext("明日不上班").foregroundColor(UIColor.blue)
}

上述是咱们的根底版别,而要完成这个,咱们需求增加上有必要的**buildBlock** 办法,来将办法中的描绘句子都结合起来。

@resultBuilder
enum NSAttributedStringBuilder {
    static func buildBlock(_ components: Component...) -> NSAttributedString {
        let mas = NSMutableAttributedString.init(string: "")
        components.forEach {
            mas.append($0.attributedString)
        }
        return mas
    }
}
// 然后咱们的初始化办法
extension NSAttributedString {
	convenience init(@NSAttributedStringBuilder _ builder: () -> NSAttributedString) {
        self.init(attributedString: builder())
    }
}

实践上运用**@NSAttributedStringBuilder** 之后会将这个闭包中的代码转换如下:

NSAttributedString {
	let a0 = Atext("老子").foregroundColor(UIColor.red)
	let a1 = Atext("明日不上班").foregroundColor(UIColor.blue)
	return NSAttributedStringBuilder.buildBlock(a0,a1)
}

支撑if句子

假如需求咱们的DSL支撑基本的条件判别呢?比方说如下:

NSAttributedString{
   Atext("老子").foregroundColor(UIColor.red)
	Atext("明日不上班").foregroundColor(UIColor.blue)
	if true {
		Atext("哈哈").foregroundColor(UIColor.black)
		Atext("哈哈").foregroundColor(UIColor.black)
	}
}

依据Apple上述供给的办法簇,咱们需求完成buildOptional 办法。要注意的是buildBlock会直接效果在挑选句子中,效果之后如下:

NSAttributedString {
	let v0 = Atext("老子").foregroundColor(UIColor.red)
	let v1 = Atext("明日不上班").foregroundColor(UIColor.blue)
	let v2: Component
	if true {
		let v2_0 = Atext("哈哈").foregroundColor(UIColor.black)
		let v2_1 = Atext("哈哈").foregroundColor(UIColor.black)
		let v2_block = NSAttributedStringBuilder.buildBlock(v2_0,v2_1)
		v2 = NSAttributedStringBuilder.buildOptional(v2_block)
	} else {
		v2 = NSAttributedStringBuilder.buildOptional(nil)
	}
	return NSAttributedStringBuilder.buildBlock(v0,v1,2)
}

所以buildOptional的参数类型有必要是buildBlock的返回值类型,所以能够完成如下:

static func buildOptional(_ component: NSAttributedString?) -> Component {
    if component == nil {
        return Atext("")
    } else {
        return Atext(component!.string, attributes: component!.attributes(at: 0, effectiveRange: nil))
    }
 }

经过这种办法,就能够在咱们自界说的DSL中运用if句子了,但是运用if-else挑选句子,挑选switch句子,以及for-in循环句子都会增加相应的静态办法,接下来的就不具体赘述了,希望大家能够写代码实践一波。

风停了

说了这么多,就聊了聊ResultBuilder,后边本来还想聊一聊DSL的,但是最近没有许多时间,这个小系列暂时这样吧,后边有时间再聊聊DSL。

参阅

  • Apple 0289 Result Builder
  • NSAttributedStringBuilder
  • 我正在参加技术社区创作者签约方案招募活动,点击链接报名投稿。