阅读 1876

声明式 UI 框架 SwiftUI 的体验及渲染流程探索

前言

在刚刚结束的 WWDC 19 上,苹果推出了可以运行在所有 Apple 平台的全新的声明式 UI 框架 —— SwiftUI。而在不久前的 Google I/O 19 上,Android 也推出了其声明式 UI 框架 —— Jetpack Compose。使得现在,不管是移动操作系统 Android 和 iOS,还是前端开发框架 React、Vue、Angular,还是现在最新的跨平台开发框架 Flutter,都加入到了声明式 UI 的阵营。本篇文章就主要讲一下 SwiftUI 的使用,以及对其渲染流程的探索。

准备

在使用 SwiftUI 之前,需要如下的环境:

  • Xcode 11 beta(必选

    通过官网下载最新的 Xcode 11 beta 并安装。

  • macOS 10.15 beta (可选

    macOS 的系统最好升级到 10.15 beta,不升也没关系。因为 10.15 beta 目前还没有公测,想要体验到 10.15 beta 目前还比较麻烦,首先需要有开发者账号,然后注册 Apple Developer Program 来获取最新的版本。但是不更新系统也是可以体验到 SwiftUI 的,只是不能使用 Xcode 里的预览界面的功能而已,所以我是没有更新 macOS 的系统,我的 macOS 的系统版本号是 10.14.5。

创建支持 SwiftUI 的工程

Xcode 11 beta 安装成功后打开,

选择 Create a new Xcode project

选择 iOS 里的 Single View App,然后点击 Next

然后输入 Product NameOrganization Identifier,最重要的是要勾选上 Use SwiftUI,然后点击 Next

选择工程的文件夹,然后点击 Create,一个使用 SwiftUI 的工程就会创建成功。

在 Xcode 里选中 ContenView.swift 就可以看到用声明式写的 UI,然后我们 build 这个工程,下面是运行的效果:

使用 SwiftUI 写一个简单的计算器

接下来,准备使用 SwiftUI 写一个简单的计算器,这个计算器有一个 Text,用来显示当前的数字,有两个按钮,一个自增的按钮,和一个自减的按钮,这三个 View 使用 VStack 来排列,VStack 可以允许竖直排列 View,HStack 允许水平排列 View。所以这个界面就可以这么写:

    
    var body: some View {
        VStack{
            Text("0")
            
            Button(
                action:{print("increment")},
                label: {Text("increment")}
            )
            
            Button(
                action: {print("decrement")},
                label: {Text("decrement")}
            )
        }
    }
}
复制代码

Button 里的 action 代表的是按钮点击事件,运行后的效果为:

如果想要改变 Text 的样式的话,可以如下这种操作:

Text(String(count))
    .frame(
        width: UIScreen.main.bounds.width,
        height: 50
    )
    .background(Color.blue)
    .foregroundColor(Color.yellow)
    .padding(10)
复制代码

运行后的效果为:

如果你之前体验过声明式的 UI 框架,对这种写法应该不会陌生。

因为要实现计算器的功能,所以需要定义一个变量来存储当前的值,这个变量的值也会显示在 Text 里,这个变量要定义在 ContentView 里,而且必须要用 @State,同时实现自增和自减的函数,代码如下:

struct ContentView : View {
    @State var count = 0;
    
    var body: some View {
        VStack{
            Text(String(count))
                .frame(
                    width: UIScreen.main.bounds.width,
                    height: 50
                )
                .background(Color.blue)
                .foregroundColor(Color.yellow)
                .padding(10)
            
            Button(
                action:{self.increment()},
                label: {Text("increment")}
            )
            
            Button(
                action: {self.decrement()},
                label: {Text("decrement")}
            )
        }
    }
    
    func increment() {
        count = count + 1
    }
    
    func decrement() {
        count = count - 1
    }
}
复制代码

运行后,点击加、减的按钮,Text 里显示的值就会跟随着变化:

SwiftUI 的渲染流程

因为 SwiftUI 并没有开源,所以不能看源代码,但是我们根据现有的资料,也足以推断出 SwiftUI 的渲染流程。

SwiftUI 里 View 的定义

在前面写计算机 UI 的时候,我们用到了如下的定义:View、VStack、Text、Button。我们可以分析一下这几个类的定义。

比如 View 在源代码里的定义为:

public protocol View : _View {

    /// 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.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}
复制代码

可以看到 View 是一个 protocol 接口。

VStack 在源代码里的定义为:

public struct VStack<Content> 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: Length? = nil, 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 是一个 struct 结构体。

Text 在源代码里的定义为:

public struct Text : Equatable {

    /// Creates an instance that displays `content` verbatim.
    public init(verbatim content: String)

    /// Creates an instance that displays `content` verbatim.
    public init<S>(_ content: S) where S : StringProtocol

    /// Creates text that displays localized content identified by a key.
    ///
    /// - Parameters:
    ///     - key: The key for a string in the table identified by `tableName`.
    ///     - tableName: The name of the string table to search. If `nil`, uses
    ///       the table in `Localizable.strings`.
    ///     - bundle: The bundle containing the strings file. If `nil`, uses the
    ///       main `Bundle`.
    ///     - comment: Contextual information about this key-value pair.
    public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)

    public func resolve(into result: inout Text._Resolved, in environment: EnvironmentValues)

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func == (a: Text, b: Text) -> Bool
}
复制代码

Text 也是一个 struct 结构体。

我们也可以用 UIKit 框架里面最基础的视图类 UIView 来进行对比,就可以发现 SwiftUI 里的 View 只是一个描述而已,并不负责实际的渲染。

SwiftUI 的渲染流程

在结合在网上其余由关 SwiftUI 的资料,和 React、Flutter 的类 Virtual DOM 的技术,可以推断 SwiftUI 的渲染流程为:

  1. 首先用声明式的方法来写 UI
  2. 然后 SwiftUI 框架内部会根据 View 的声明,来渲染 UI
  3. 当状态发生变化时,首先会通过前后 View 声明的变化,确定哪些 UI 有变化,然后再去渲染变化的 UI,这样就保证了不会重复渲染,而且 View 的声明都是 struct 结构体,它的创建和销毁是很轻量的,不会对性能造成影响,从而保证了渲染的效率。

很可惜 SwiftUI 是闭源的,没办法知道里面细节的实现,但是大致的渲染过程应该是这个样子了。

现在能开始使用 SwiftUI 吗?

SwiftUI 是 iOS 13 才有的全新 framework,所以 iOS 13 以前的 iPhone & iPad 是不支持的。而 iOS 13 也是 2019 年 6 月 4 日 WWDC 19 上才发布的,虽然 iOS 的系统的升级率比较高,但那也得一年以后才能开始用于正式的生产环境。

关注下面的标签,发现更多相似文章
评论