SwiftUI之ViewModifier詳解

本篇文章將帶領(lǐng)大家一起學(xué)習(xí)SwiftUI中的ViewModifier帅霜,通過學(xué)習(xí)ViewModifier匆背,我們可以了解Swift中的@_functionBuilder

大家先看下邊這段代碼:

VStack {
    Text("abc")
    Spacer()
    Text("def")
}

在SwiftUI中身冀,這樣的代碼太常見了钝尸,但大家有沒有思考過,在大括號(hào)中間搂根,放了幾個(gè)view珍促,這幾個(gè)view是如何添加到父view上的呢?

我們先看一個(gè)普通的函數(shù):

  func test(_ content: () -> String) -> Void {
      print(content())
  }

這是一個(gè)很普通的函數(shù)剩愧,但是函數(shù)的參數(shù)猪叙,我們傳遞了一個(gè)閉包,接下來, 我們調(diào)用這個(gè)函數(shù):

  Button("test") {
      self.test {
          "abc"
      }
  }

當(dāng)閉包作為最后一個(gè)參數(shù)時(shí)沐悦,我們可以像上邊這些寫代碼成洗,那么五督,我為什么要演示上邊的這個(gè)函數(shù)調(diào)用呢藏否?請(qǐng)大家再仔細(xì)看這段代碼:

VStack {
    Text("abc")
    Spacer()
    Text("def")
}

大家明白了嗎? 上邊的閉包其實(shí)就是VStack的一個(gè)初始化函數(shù)的最后一個(gè)參數(shù)充包,跟上邊我們演示的函數(shù)沒什么兩樣副签。我們?cè)倮^續(xù)看看其函數(shù)定義:

/// 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
}

通過分析,我們可以發(fā)現(xiàn)以下幾點(diǎn):

  • VStack是一個(gè)結(jié)構(gòu)體
  • 其初始化函數(shù)的最后一個(gè)參數(shù)為@ViewBuilder content: () -> Content基矮,該函數(shù)與普通函數(shù)的區(qū)別在于前邊有一個(gè)@ViewBuilder

那么這個(gè)@ViewBuilder是什么東西呢淆储?我們繼續(xù)看它的定義:

@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本身也是一個(gè)結(jié)構(gòu)體家浇,但是它用了@_functionBuilder修飾本砰,那么@_functionBuilder有什么用呢?

@_functionBuilder能夠讓我們對(duì)函數(shù)做一層轉(zhuǎn)換钢悲,這是它最大的用處点额,我們舉個(gè)簡單的例子:

@_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())
    }
 }

當(dāng)我們點(diǎn)擊按鈕后,可以打印出:

["a", "b", "c", "d"]

大家明白了嗎莺琳? 通過@_functionBuilder还棱,我們就可以獲取函數(shù)中的變量,然后拿著這些數(shù)據(jù)做一些額外的事情惭等。

上邊的代碼珍手,是我們自己實(shí)現(xiàn)的一個(gè)builder,目的是把變量放到一個(gè)數(shù)組中辞做,那么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最終轉(zhuǎn)成了TupleView秤茅,在上邊代碼中的最后一個(gè)extension中稚补,最多只能接受10個(gè)view,這也就是在SwiftUI中的容器類型最多可以放10個(gè)view的原因嫂伞。

當(dāng)然孔厉,我們?nèi)绻敕鸥嗟膙iew,可以通過Group或者ForEach來實(shí)現(xiàn)帖努。

我們?cè)偕钊胍稽c(diǎn)撰豺,大家看下邊的代碼:

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表達(dá)式,特意擴(kuò)展了一些東西:

@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
}

知道了這些知識(shí)后拼余,我們平時(shí)該如何使用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)
    }
}

目的是能夠開發(fā)出類似上邊代碼這樣的view, 可以為自定義的view擴(kuò)展其他的view匙监。

到目前為止凡橱,我們已經(jīng)了解了ViewBuilder的原理小作,我們還可以使用@_functionBuilder做一些更有趣的事情:

如果我們想在某個(gè)頁面中彈出一個(gè)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轉(zhuǎn)換成UIAlertAction,然后添加到UIAlertController中坝撑,有了上邊我們講解的知識(shí)静秆,大家應(yīng)該能夠理解這些代碼。

我們?cè)陂_發(fā)中這樣使用:

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)

重點(diǎn)是巡李,基于這些用法抚笔,我們可以開發(fā)出很多其他的Builders,再舉一個(gè)網(wǎng)上的例子:

NSAttributedString {
  AText("Hello world")
    .font(.systemFont(ofSize: 24))
    .foregroundColor(.red)
  LineBreak()
  AText("with Swift")
     .font(.systemFont(ofSize: 20))
     .foregroundColor(.orange)
}

更多內(nèi)容侨拦,參考這個(gè)網(wǎng)站https://github.com/carson-katri/awesome-function-builders

總結(jié)

我們從SwiftUI中的VStack開始殊橙,學(xué)習(xí)了ViewBuilder的用法和原理,又學(xué)習(xí)了@_functionBuilder的用法狱从,最后我們舉了兩個(gè)例子來演示如何自定義函數(shù)Builder膨蛮。這些技術(shù)可以在Swift中做各種各樣的擴(kuò)展,全憑大家的想象力矫夯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸽疾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子训貌,更是在濱河造成了極大的恐慌制肮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件递沪,死亡現(xiàn)場(chǎng)離奇詭異豺鼻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)款慨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門儒飒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人檩奠,你說我怎么就攤上這事桩了。” “怎么了埠戳?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵井誉,是天一觀的道長。 經(jīng)常有香客問我整胃,道長颗圣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮在岂,結(jié)果婚禮上奔则,老公的妹妹穿的比我還像新娘。我一直安慰自己蔽午,他們只是感情好易茬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著祠丝,像睡著了一般疾呻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上写半,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音尉咕,去河邊找鬼叠蝇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛年缎,可吹牛的內(nèi)容都是我干的悔捶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼单芜,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蜕该!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起洲鸠,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤堂淡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后扒腕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绢淀,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年瘾腰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了皆的。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蹋盆,死狀恐怖费薄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情栖雾,我是刑警寧澤楞抡,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站岩灭,受9級(jí)特大地震影響拌倍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一柱恤、第九天 我趴在偏房一處隱蔽的房頂上張望数初。 院中可真熱鬧,春花似錦梗顺、人聲如沸泡孩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仑鸥。三九已至,卻和暖如春变屁,著一層夾襖步出監(jiān)牢的瞬間眼俊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工粟关, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疮胖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓闷板,卻偏偏與公主長得像澎灸,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子遮晚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容