SwiftUI之ViewModifier详解

本篇文章将带领大家一起学习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
}

通过分析,我们可以发现以下几点:

  • VStack是一个结构体
  • 其初始化函数的最后一个参数为@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)
}

更多内容,参考这个网站https://github.com/carson-katri/awesome-function-builders

总结

我们从SwiftUI中的VStack开始,学习了ViewBuilder的用法和原理,又学习了@_functionBuilder的用法,最后我们举了两个例子来演示如何自定义函数Builder。这些技术可以在Swift中做各种各样的扩展,全凭大家的想象力。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容