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)
}
复制代码

更多内容,参考这个网站 github.com/carson-katr…

总结

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章