前言
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
是毫无疑问的: