这是一篇来自 Majid 的文章的翻译,并且增加了部分内容,便于理解。
本文介绍了目前已知的部分 SwiftUI 新特性。由于今天是 WWDC 的第一天,目前官方只放出了 Platforms State of the Union 这一支技术视频,因此本文只覆盖到了视频内提到的所有新特性。后续新特性会在之后几天中陆续介绍(如果有时间的话)。
本文中出现的新 API 需要搭配 Xcode 15 beta 使用,并且部分 API 还没有实装。
WWDC 23如期而至,很多内容也随之更新并被添加到 SwiftUI 框架中。阅读本文,你可以了解到 SwiftUI 框架第5次迭代中新增的几个最重要的特性。
数据流
与 UIKit 不同,SwiftUI 使用新的观察框架(Observation framework)作为其数据流。观察框架提供了一个 Observable
协议,我们通过它来订阅更改和更新 SwiftUI 视图。不过之前 SwiftUI 的数据流 API 比较复杂,需要了解的细节很多,上手成本颇高。
不过随着 Swift 5.9 引入了宏特性,Observation framework 利用宏优化了大量 API ,精简和隐藏了部分逻辑。
@Observable
现在不需要让类(ViewModel)遵循 Observable
协议,直接使用 @Observer
宏标记就行,这个宏会自动让该类遵循 Observable
协议。现在也不需要给属性前加 @Published
,因为 SwiftUI 视图会自动跟踪任何可观察类型的可用属性的更改。
// before
final class Store: ObservableObject {
@Published var products: [String] = []
@Published var favorites: [String] = []
func fetch() async {
try? await Task.sleep(nanoseconds: 1_000_000_000)
// load products
products = [
"Product 1",
"Product 2"
]
}
}
// now
@Observable
final class Store {
var products: [String] = []
var favorites: [String] = []
func fetch() async {
try? await Task.sleep(nanoseconds: 1_000_000_000)
// load products
products = [
"Product 1",
"Product 2"
]
}
}
此处开发人员还提到:
Observable lets SwiftUI track access at a per-field level, so your view’s body is only re-evaluated when the specific properties used by your view change. If you modify a field not used by your view, no invalidation will happen at all.
新的监听方式会让 UI 刷新的范围限定在更改过的属性上,因此不会出现之前一个 ViewModel 中的某一个属性更改,所有引用到 ViewModel 的 View 都会刷新的问题,算是大大提升了优化效率。
@State
此前我们有很多属性包装器,比如 State
、 StateObject
、 ObservedObject
和 EnvironmentObject
,开发者需要明确在什么时候使用什么类型的包装器。现在,状态管理变得更简单了。对于值类型(String
、Int
等)以及符合 Observable
协议的引用类型(如 ViewModel ),无脑使用 @State
标记就行了。
struct ProductsView: View {
// @State private var text = ""
// @StateObject private var store = Store()
let store = Store()
var body: some View {
List(store.products, id: .self) { product in
Text(verbatim: product)
}
.task {
if store.products.isEmpty {
await store.fetch()
}
}
}
}
由于 @State
完全抹平了 State
、 StateObject
、 ObservedObject
和 EnvironmentObject
的差异,因此 @State private var store = Store()
这段代码定义的 store
不仅仅是一个 StateObject
,也是 EnvironmentObject
。简单来讲,之前 Apple 把复杂的内部实现暴露出来的,但是从新版 SwiftUI 开始,你不需要再了解这些细节,只需要了解一种状态类型 State
即可。既然如此,我们可以玩一个小技巧。下面的代码, store
不仅仅是 ProductsView
内部的状态类,也被 .environment(store)
直接变为了 EnvironmentObject
注入到了所有子视图了。EnvironmentViewExample
就可以通过 Environment API 拿到 store
实例。
struct EnvironmentViewExample: View {
@Environment(Store.self) private var store
var body: some View {
Button("Fetch") {
Task {
await store.fetch()
}
}
}
}
struct ProductsView: View {
@State private var store = Store()
var body: some View {
List(store.products, id: .self) { product in
Text(verbatim: product)
}
.task {
if store.products.isEmpty {
await store.fetch()
}
}
.toolbar {
NavigationLink {
EnvironmentViewExample()
} label: {
Text(verbatim: "Environment")
}
}
.environment(store)
}
}
@ObservedObject
这个例子中,我们有一个视图,它有一个从外部传进来的参数 store
。在此之前,我们需要使用 @ObservedObject
属性包装器来标记它,以订阅它的更改。现在不需要了,因为 SwiftUI 视图会自动跟踪符合 Observable
协议的类型的更改。
struct FavoriteProductsView: View {
// @ObservedObject let store: Store
let store: Store
var body: some View {
List(store.favorites, id: .self) { product in
Text(verbatim: product)
}
}
}
@Bindable
struct BindanbleViewExample: View {
@Bindable var store: Store
var body: some View {
List($store.products, id: .self) { $product in
TextField(text: $product) {
Text(verbatim: product)
}
}
}
}
对于双向绑定(Binding),之前只能将基础类型包装成 Binding 传递给子视图,现在新增了一个 @Bindable
,可以用来标记引用类型,这样就能直接把 ViewModel 传递给子视图了。
动画
动画是 SwiftUI 框架中最重要的部分,在 SwiftUI 中给任何东西添加动画都是轻而易举的。此次更新为 SwiftUI 动画增加了一些新特性。
withAnimation completion
正如例子中看到的,withAnimation API 新增了 completion 回调,方便我们对动画进行控制。
struct AnimationExample: View {
@State private var value = false
var body: some View {
Text(verbatim: "Hello")
.scaleEffect(value ? 2 : 1)
.onTapGesture {
withAnimation {
value.toggle()
} completion: {
print("Animation have finished")
}
}
}
}
PhaseAnimator
SwiftUI 框架引入了新的 PhaseAnimator 类,允许为每个阶段提供不同的动画,并在阶段更改时更新内容。这个其实属于一个工具类,方便我们做一些拥有不同状态或阶段的动画。没有这个类也能做,就是稍微麻烦点。
enum Phase: CaseIterable {
case start
case loading
case finish
}
struct PhasedAnimationExample: View {
@State private var value = false
var body: some View {
PhaseAnimator(Phase.allCases, trigger: value) { phase in
switch phase {
case .start:
StartPhaseView()
.onTapGesture {
value.toggle()
}
case .loading:
LoadingPhaseView()
case .finish:
FinishPhaseView()
}
} animation: { phase in
switch phase {
case .start: .easeIn(duration: 0.3)
case .loading: .easeInOut(duration: 0.5)
case .finish: .easeOut(duration: 0.1)
}
}
}
}
KeyframeAnimator
今年 SwiftUI 也带来了关键帧动画,应该能在动画自由度上再上一层楼。但是目前没有看到相关 API,官方演示的 demo 是 MapKit 中的使用案例。
ScrollView
ScrollView 今年有很好的补充。首先,我们可以使用 scrollPosition
视图修饰符来控制和观察内容偏移。
struct ContentView: View {
@State private var scrollPosition: Int? = 0
var body: some View {
ScrollView {
Button("Scroll") {
scrollPosition = 80
}
ForEach(1..<100, id: .self) { number in
Text(verbatim: number.formatted())
}
.scrollTargetLayout()
}
.scrollPosition(id: $scrollPosition)
}
}
scrollTargetBehavior
新增 scrollTargetBehavior
, 支持分页模式。
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1..<100, id: .self) { number in
Text(verbatim: number.formatted())
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
}
搜索
现在可以使用 searchable
视图修饰符的 isPresent
参数来显示/隐藏搜索字段。还可以使用 searchScope 来限定搜索范围。
struct ProductsView: View {
@State private var store = Store()
@State private var query = ""
@State private var scope: Scope = .default
var body: some View {
List(store.products, id: .self) { product in
Text(verbatim: product)
}
.task {
if store.products.isEmpty {
await store.fetch()
}
}
.searchable(text: $query, isPresented: .constant(true), prompt: "Query")
.searchScopes($scope, activation: .onTextEntry) {
Text(verbatim: scope.rawValue)
}
}
}
手势
新增 RotateGesture
和 MagnifyGesture
, 允许我们跟踪视图的旋转和放大。
struct RotateGestureView: View {
@State private var angle = Angle(degrees: 0.0)
var rotation: some Gesture {
RotateGesture()
.onChanged { value in
angle = value.rotation
}
}
var body: some View {
Rectangle()
.frame(width: 200, height: 200, alignment: .center)
.rotationEffect(angle)
.gesture(rotation)
}
}
其他改进
ContentUnavailableView
新增 ContentUnailableView
视图,可以在需要的时候显示空视图,展示一个图片和一行文字。算是一个很小的改进吧。
struct ProductsView: View {
ContentUnavailableView("Products list is empty", systemImage: "list.dash")
}
#Preview
新增 #Preview
宏,替代原来复杂的 preview 代码。不过 preview 模版代码基本上都是新建 View 的时候模板自动生成的,影响不大。
// before
struct PlayerView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// now
#Preview {
ContentView()
}
其他
新增一对新的视图修饰符,允许我们调整列表中的间距。您可以使用 listRowSpaces
和 listSectionSpaces
视图修饰符来设置列表中所需的间距。
EnvironmentValues
结构包含一系列与最新平台更新相关的新属性,比如 isActivityFullscreen 和 showsWidgetContainerBack。
Swift Charts 支持滚动。
SF Symbols 增加动画效果。
总结
每一年的 SwiftUI 迭代都会让我感觉,这些新特性明明应该早就有才对。像是 @State
这样的能力,如果一开始就有,会大大降低 SwiftUI 学习的门槛。不过这次 API 上的改动很多都依赖于 Swift 宏,而宏直到 5.9 才正式发布,也算是可以理解吧。不过按照这样的节奏下去,不知道何年马月才能真正大面积用上 SwiftUI 。据说今年 Platforms State of the Union
上总共就提了 3 次 UIKit,其中一次还是讲的 #Preview
支持预览 UIKit ?。看来我们将会在很长一段时间内卡在两代技术中间。或者,投入另一个新技术:realityOS?