前言
SwiftUI
编写更少的代码,打造更出色的 app。
SwiftUI 是一种创新、简洁的编程方式,通过 Swift 的强大功能,在所有 Apple 平台上构建用户界面。借助它,您只需一套工具和 API,即可创建面向任何 Apple 设备的用户界面。SwiftUI 采用简单易懂、编写方式自然的声明式 Swift 语法,可无缝支持新的 Xcode 设计工具,让您的代码与设计保持高度同步。 SwiftUI 原生支持“动态字体”、“深色模式”、本地化和辅助功能——第一行您写出的 SwiftUI 代码,就已经是您编写过的、功能最强大的 UI 代码。 developer.apple.com/xcode/swift…
Creating and Combining Views
关于 some View
新建一个 SwiftUI 的新项目,会出现如下代码:一个Text展示在body中。
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
可能会对some比较陌生,所以我们先从View说起。
文档内可以看到View是 SwiftUI 一个协议,这个协议里含有一个associatedtype:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol 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
/// The content and behavior of the view.
@ViewBuilder var body: Self.Body { get }
}
带有这种修饰的协议不能作为类型来使用,只能当作约束来使用。通过some View的修饰,会向编译器保证:每次闭包中返回的一定是一个确定,而且遵守View协议的类型,不要去关心到底是哪种类型。
// Error
func createView() -> View {
}
// Correct
func createView<T: View>() -> T {
}
这种写法使用了 Swift 5.1 的 Opaque return types特性。这样的设计,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不需要修改公共API来确定每次闭包的返回类型,也降低了代码书写难度。
Preview SwiftUI
SwiftUI 含有苹果对标 React Native 或 Flutter 的 Hot Reloading 工具,Xcode 将对代码进行静态分析,找到所有遵守PreviewProvider协议的类型进行预览渲染。经过尝试,Preview SwiftUI 不需要运行 app 就能查看实时预览,不像其他 Hot Reloading 可能还需要重启 app 或进入对应界面,调整数据进行调试。
快捷键:Option + Command + P
关于 ViewBuilder
我们先来看一个最简单的 UI:
我们创建了一个横向布局组件
Hstack,里面有两个文字组件Text。问题是这两个Text不是以数组的形式放进content的闭包里,那这种写法为什么还能成立?我们来看Hstack的初始化方法:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct HStack<Content> : View where Content : View {
/// Creates a horizontal stack with the given spacing and vertical alignment.
///
/// - Parameters:
/// - alignment: The guide for aligning the subviews in this stack. This
/// guide has the same vertical screen coordinate for every child view.
/// - spacing: The distance between adjacent subviews, or `nil` if you
/// want the stack to choose a default distance for each pair of
/// subviews.
/// - content: A view builder that creates the content of this stack.
@inlinable public init(alignment: VerticalAlignment = .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
}
看最后一个参数content,只返回了一个() -> Content类型,但我们在创建的时候只是列举了两个Text,并没有返回一个可用的Content。
这里使用的就是 Swift 5.1 的另一个特性 Function Builder,同时注意到content前面有一个@ViewBuilder标记。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {
/// Builds an empty view from a block containing no statements.
public static func buildBlock() -> EmptyView
/// Passes a single view written as a child view through unmodified.
///
/// An example of a single view written as a child view is
/// `{ Text("Hello") }`.
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
被@_functionBuilder标记的类型可以被用来对其他类型进行标记,所以在这个结构体中,@ViewBuilder可以对content进行标记,所以被标记的content会按照ViewBuilder中合适的buildBlock进行 build 后再使用。
有趣的是我们发现了文档中有这么多buildBlock:
@available(iOS 13.0, macOS 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, macOS 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, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0 : View, C1 : View, C2 : View, C3 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View
}
@available(iOS 13.0, macOS 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
}
而事实上,刚才我所写的示例代码也有如下写法:
而基于 function builder 的构造方式是有一定限制的,文档里规定了最多 10 个参数。
Building Lists and Navigation
List
最简单的办法是创建一个静态List,如下:
这里的
List和HStack或VStack很相似,接受一个 view builder 并采用 View DSL 的方式列举了几个 Row。这种方式构建了对应着 UITableView 的静态 cell 的组织方式。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension List where SelectionValue == Never {
/// Creates a list with the given content.
///
/// - Parameter content: The content of the list.
public init(@ViewBuilder content: () -> Content)
}
同样我们去查看List的初始化方法,还有其他一些动态初始化方法:
public init<RowContent>(_ data: Range<Int>, @ViewBuilder rowContent: @escaping (Int) -> RowContent) where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent : View
NavigationView
可以通过NavigationView来进行页面跳转:
Handling User Input
@State
在前端界面上,数据同步及时刷新比较重要。一般都是数据源数据更新了,界面 UI 同时更新。在 SwiftUI 里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,因此,只需要一次布局。在属性前面加上@State关键词,即可实现每次数据改动,UI 动态更新的效果。
这个示例通过
@State管理私有状态value并驱动展示。点击不同的按钮分别会 + 1 和 - 1,显示的数字也会跟着改变。
接下来看一下文档中的State:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
public init(wrappedValue value: Value)
public init(initialValue value: Value)
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
}
注意到State是一个结构体,并且由@propertyWrapper包装的。@propertyWrapper是属性包装器,@State Int不等同于Int。property wrapper 做的事情大体如下:
- 为底层的存储变量
State<Int>自动提供一组 getter 和 setter 方法,结构体内保存了Int的具体数值; - 在 body 首次求值前,将
State<Int>关联到当前View上,为它在堆中对应当前View分配一个存储位置。 - 为
@State修饰的变量设置观察,当值改变时,触发新一次的body求值,并刷新 UI。
State内部
State内部大致如下:
struct State<Value> : DynamicProperty {
var _value: Value
var _location: StoredLocation<Value>?
var _graph: ViewGraph?
var wrappedValue: Value {
get { _value }
set {
updateValue(newValue)
}
}
// 发生在 init 后,body 求值前。
func _linkToGraph(graph: ViewGraph) {
if _location == nil {
_location = graph.getLocation(self)
}
if _location == nil {
_location = graph.createAndStore(self)
}
_graph = graph
}
func _renderView(_ value: Value) {
if let graph = _graph {
// 有效的 State 值
_value = value
graph.triggerRender(self)
}
}
}
对于@State的声明,会在当前View中带来一个自动生成的私有存储属性,来存储真实的State struct值。如下:
struct DetailView: View {
@State private var value: Int?
private var _value: State<Int?> // 自动生成
// ...
}
所以如果声明@State var value: Int无法编译,因为Int?的声明在初始化时会默认赋值为nil,让_value完成初始化 (它的值为 State<Optional<Int>>(_value: nil, _location: nil));而非Optional的value则需要明确的初始化值,否则在调用self.value的时候,底层_value是没有完成初始化的。对于@State的设置,只有在 View 被添加到 graph 中以后 (也就是首次 body 被求值前) 才有效。
State, Binding, StateObject, ObservedObject
@StateObject的情况和@State很类似:View都拥有对这个状态的所有权,它们不会随着新的View init而重新初始化。这个行为和Binding以及ObservedObject是正好相反的:使用Binding和ObservedObject的话,意味着View不会负责底层的存储,开发者需要自行决定和维护“非所有”状态的声明周期。
所以有这么个场景:subView 想使用 contentView 的值,并将这个值及时反馈回 contentView 中,使用@Binding是毫无疑问的: