本篇文章将带领大家一起学习SwiftUI中的ViewModifier,通过学习ViewModifier,我们可以了解Swift中的@_functionBuilder
。
大家先看下边这段代码:
VStack { Text("abc") Spacer() Text("def") } 复制代码
在SwiftUI中,这样的代码太常见了,但大家有没有思考过,在大括号中间,放了几个view,这几个view是如何添加到父view上的呢?
我们先看一个普通的函数:
func test(_ content: () -> String) -> Void { print(content()) } 复制代码
这是一个很普通的函数,但是函数的参数,我们传递了一个闭包,接下来, 我们调用这个函数:
Button("test") { self.test { "abc" } } 复制代码
当闭包作为最后一个参数时,我们可以像上边这些写代码,那么,我为什么要演示上边的这个函数调用呢?请大家再仔细看这段代码:
VStack { Text("abc") Spacer() Text("def") } 复制代码
大家明白了吗? 上边的闭包其实就是VStack的一个初始化函数的最后一个参数,跟上边我们演示的函数没什么两样。我们再继续看看其函数定义:
/// A view that arranges its children in a vertical line. @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @frozen public struct VStack<Content> : View where Content : View { /// Creates an instance with the given `spacing` and Y axis `alignment`. /// /// - Parameters: /// - alignment: the guide that will have the same horizontal screen /// coordinate for all children. /// - spacing: the distance between adjacent children, or nil if the /// stack should choose a default distance for each pair of children. @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) /// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required `body` property. public typealias Body = Never } 复制代码
通过分析,我们可以发现以下几点:
@ViewBuilder content: () -> Content
,该函数与普通函数的区别在于前边有一个@ViewBuilder
那么这个@ViewBuilder
是什么东西呢?我们继续看它的定义:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @_functionBuilder public struct ViewBuilder { /// Builds an empty view from an block containing no statements, `{ }`. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through /// unmodified. public static func buildBlock<Content>(_ content: Content) -> Content where Content : View } @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view /// that is visible only when the `if` condition evaluates `true`. public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View /// Provides support for "if" statements in multi-statement closures, producing /// ConditionalContent for the "then" branch. public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View /// Provides support for "if-else" statements in multi-statement closures, producing /// ConditionalContent for the "else" branch. public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View } 复制代码
可以看出,ViewBuilder本身也是一个结构体,但是它用了@_functionBuilder
修饰,那么@_functionBuilder
有什么用呢?
@_functionBuilder
能够让我们对函数做一层转换,这是它最大的用处,我们举个简单的例子:
@_functionBuilder struct TestBuilder { static func buildBlock(_ items: String...) -> [String] { items } } 复制代码
struct ContentView: View { @State private var text = "ccc" var body: some View { VStack { Button("test") { self.test { "a" "b" "c" "d" } } } } func test(@TestBuilder _ content: () -> [String]) -> Void { print(content()) } } 复制代码
当我们点击按钮后,可以打印出:
["a", "b", "c", "d"] 复制代码
大家明白了吗? 通过@_functionBuilder
,我们就可以获取函数中的变量,然后拿着这些数据做一些额外的事情。
上边的代码,是我们自己实现的一个builder,目的是把变量放到一个数组中,那么ViewModifier做了什么事情呢?
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View } @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) 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 } ... @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View } 复制代码
很明显,ViewBuilder把我们输入的view最终转成了TupleView,在上边代码中的最后一个extension中,最多只能接受10个view,这也就是在SwiftUI中的容器类型最多可以放10个view的原因。
当然,我们如果想放更多的view,可以通过Group或者ForEach来实现。
我们再深入一点,大家看下边的代码:
struct ContentView: View { @State private var hasText = false @State private var show = false var body: some View { VStack { Text("a") if hasText { Text("b") } if show { Text("d") } else { Text("") } Text("c") } } } 复制代码
ViewBuilder为了支持闭包中的if表达式,特意扩展了一些东西:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @_functionBuilder public struct ViewBuilder { /// Builds an empty view from an block containing no statements, `{ }`. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through /// unmodified. public static func buildBlock<Content>(_ content: Content) -> Content where Content : View } @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view /// that is visible only when the `if` condition evaluates `true`. public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View /// Provides support for "if" statements in multi-statement closures, producing /// ConditionalContent for the "then" branch. public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View /// Provides support for "if-else" statements in multi-statement closures, producing /// ConditionalContent for the "else" branch. public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View } 复制代码
知道了这些知识后,我们平时该如何使用ViewBuilder呢?
struct ContentView: View { @State private var hasText = false @State private var show = true var body: some View { CustomView(Color.orange) { Text("aaaa") } } } struct CustomView<T: View>: View { let bgColor: Color var content: T init(_ bgColor: Color, @ViewBuilder _ content: () -> T) { self.bgColor = bgColor self.content = content() } var body: some View { self.content .background(self.bgColor) } } 复制代码
目的是能够开发出类似上边代码这样的view, 可以为自定义的view扩展其他的view。
到目前为止,我们已经了解了ViewBuilder的原理,我们还可以使用@_functionBuilder做一些更有趣的事情:
如果我们想在某个页面中弹出一个Action,需要写下边这样的代码:
let alert = UIAlertController( title: "Delete all data?", message: "All your data will be deleted!", preferredStyle: .alert) let deleteAction = UIAlertAction(title: "Yes, Delete it All", style: .destructive) { (_) in print("Deleting all data") } let moreOptionsAction = UIAlertAction(title: "Show More Options", style: .default) { (_) in print("Show more options") } let cancelAction = UIAlertAction(title: "No, Don't Delete Anything", style: .cancel, handler: nil) alert.addAction(deleteAction) alert.addAction(moreOptionsAction) alert.addAction(cancelAction) present(alert, animated: true) 复制代码
使用@_functionBuilder的黑魔法后, 我们的代码编程这样:
typealias RAlertActionHandler = () -> Void protocol RAlertAction { var title: String { get } var style: UIAlertAction.Style { get } var action: RAlertActionHandler { get } } struct DefaultAction: RAlertAction { let title: String let style: UIAlertAction.Style let action: RAlertActionHandler init(_ title: String, action: @escaping RAlertActionHandler = {}) { self.title = title self.style = .default self.action = action } } struct CancelAction: RAlertAction { let title: String let style: UIAlertAction.Style let action: RAlertActionHandler init(_ title: String, action: @escaping RAlertActionHandler = {}) { self.title = title self.style = .cancel self.action = action } } struct DestructiveAction: RAlertAction { let title: String let style: UIAlertAction.Style let action: RAlertActionHandler init(_ title: String, action: @escaping RAlertActionHandler = {}) { self.title = title self.style = .destructive self.action = action } } 复制代码
上边代码定义了几种不同样式的Action
@_functionBuilder struct RAlertControllerBuilder { static func buildBlock(_ components: RAlertAction...) -> [UIAlertAction] { components.map { action in UIAlertAction(title: action.title, style: action.style) { _ in action.action() } } } } // MARK:- UIAlertController extension UIAlertController { convenience init(title: String, message: String, style: UIAlertController.Style = .alert, @RAlertControllerBuilder build: () -> [UIAlertAction]) { let actions = build() self.init(title: title, message: message, preferredStyle: style) actions.forEach { self.addAction($0) } } } 复制代码
这段代码,把RAlertAction转换成UIAlertAction,然后添加到UIAlertController中,有了上边我们讲解的知识,大家应该能够理解这些代码。
我们在开发中这样使用:
let alert = UIAlertController( title: "Delete all data?", message: "All your data will be deleted!") { DestructiveAction("Yes, Delete it All") { print("Deleting all data") } DefaultAction("Show More Options") { print("showing more options") } CancelAction("No, Don't Delete Anything") } present(alert, animated: true) 复制代码
重点是,基于这些用法,我们可以开发出很多其他的Builders,再举一个网上的例子:
NSAttributedString { AText("Hello world") .font(.systemFont(ofSize: 24)) .foregroundColor(.red) LineBreak() AText("with Swift") .font(.systemFont(ofSize: 20)) .foregroundColor(.orange) } 复制代码
更多内容,参考这个网站github.com/carson-katr…
我们从SwiftUI中的VStack开始,学习了ViewBuilder的用法和原理,又学习了@_functionBuilder的用法,最后我们举了两个例子来演示如何自定义函数Builder。这些技术可以在Swift中做各种各样的扩展,全凭大家的想象力。