SwiftUI之ViewModifier詳解

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

大家先看下邊這段代碼:

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

在SwiftUI中,這樣的代碼太常見(jiàn)了,但大家有沒(méi)有思考過(guò),在大括號(hào)中間,放了幾個(gè)view,這幾個(gè)view是如何添加到父view上的呢?

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

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

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

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

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

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

大家明白了嗎? 上邊的閉包其實(shí)就是VStack的一個(gè)初始化函數(shù)的最后一個(gè)參數(shù),跟上邊我們演示的函數(shù)沒(méi)什么兩樣。我們?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
}

通過(guò)分析,我們可以發(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è)簡(jiǎn)單的例子:

@_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"]

大家明白了嗎? 通過(guò)@_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中的容器類(lèi)型最多可以放10個(gè)view的原因。

當(dāng)然,我們?nèi)绻敕鸥嗟膙iew,可以通過(guò)Group或者ForEach來(lái)實(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)
    }
}

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

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

如果我們想在某個(gè)頁(yè)面中彈出一個(gè)Action,需要寫(xiě)下邊這樣的代碼:

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è)陂_(kāi)發(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)是,基于這些用法,我們可以開(kāi)發(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開(kāi)始,學(xué)習(xí)了ViewBuilder的用法和原理,又學(xué)習(xí)了@_functionBuilder的用法,最后我們舉了兩個(gè)例子來(lái)演示如何自定義函數(shù)Builder。這些技術(shù)可以在Swift中做各種各樣的擴(kuò)展,全憑大家的想象力。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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