Highlight
SwiftUI 在 WWDC23 获得跨平台能力扩展:新增 visionOS 场景类型(WindowGroup、Volume、ImmersiveSpace)、@Observable 宏替代 ObservableObject、SwiftData 原生集成、KeyframeAnimator 和 PhaseAnimator 动画 API,以及 ScrollView 的滚动过渡和对齐能力。
核心内容
SwiftUI 进入空间计算时代
以前,开发 3D 界面需要学习一整套全新的框架。visionOS 发布后,这个门槛被打破了。
(01:55)SwiftUI 在 visionOS 上支持三种场景类型。WindowGroup 渲染为 2D 窗口,控件自带深度敏感的 3D 效果。WindowGroup 里可以照常使用 NavigationSplitView、TabView 等容器。
(02:26)把场景声明为 .volumetric 样式,就得到一个 Volume。Volume 在有限空间内展示 3D 内容,比如棋盘游戏或建筑模型,和其他应用并排显示。用 Model3D 展示静态模型,用 RealityView 展示带光照和交互的动态模型。
(02:59)ImmersiveSpace 场景类型定义沉浸式空间体验,支持 mixed 和 full 两种 immersion 样式。mixed 样式把虚拟内容叠加到真实环境,full 样式则让应用完全接管用户的视野。
watchOS 10 的全新设计
(04:08)watchOS 10 采用全屏彩色设计。NavigationSplitView 和 NavigationStack 获得新的转场动画,TabView 新增由数码表冠驱动的垂直分页样式。
(04:53)containerBackground 修饰符为导航和标签视图配置微妙的背景渐变,在 push 和 pop 时带动画过渡。containerBackground 也支持 TabView。
小组件进入更多场景
(05:54)小组件出现在更多位置:iPadOS 17 的锁屏、iPhone 的 Standby 模式、macOS Sonoma 的桌面。更关键的是,小组件现在支持交互控件。
(06:32)Toggle 和 Button 可以通过 App Intents 触发应用内代码,还能用 SwiftUI 的 transition 和 animation 修饰符给小组件添加动画。
数据流革命:@Observable 和 SwiftData
(10:44)@Observable 宏是 SwiftUI 数据流的最大升级。给类加上 @Observable,不需要标记 @Published,SwiftUI 自动追踪读取的属性,只在真正用到的属性变化时刷新视图。
(13:16)SwiftData 是全新的数据建模和管理框架。把 @Observable 换成 @Model 宏,模型就获得持久化能力,同时保留 Observable 的所有好处。用 Query 属性包装器从数据库获取数据,数据变化时视图自动更新。
详细内容
visionOS 场景类型
(01:55)visionOS 上的 WindowGroup 和 iOS 上写法一样:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
关键点:
WindowGroup在 visionOS 上渲染为 2D 平面窗口- 窗口内的控件自动获得深度敏感的 3D 效果
- 容器如
NavigationSplitView、TabView可直接使用
Volume 需要设置 volumetric 样式:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(.volumetric)
}
}
关键点:
.windowStyle(.volumetric)把窗口变为 3D 体积- 用
Model3D加载静态 USDZ 模型 - 用
RealityView创建带光照、物理、交互的动态 3D 场景
ImmersiveSpace 的声明方式:
@main
struct MyApp: App {
var body: some Scene {
ImmersiveSpace(id: "space") {
ImmersiveView()
}
.immersionStyle(selection: .constant(.mixed), in: .mixed, .full)
}
}
关键点:
ImmersiveSpace是独立的场景类型.immersionStyle指定支持的沉浸模式- mixed 模式把内容叠加到真实环境
- full 模式完全接管用户视野
@Observable 宏
(10:44)以前用 ObservableObject 需要给每个属性加 @Published:
class DogModel: ObservableObject {
@Published var dogs: [Dog] = []
@Published var isFavorite: Bool = false
}
现在只需一行:
@Observable class DogModel {
var dogs: [Dog] = []
var isFavorite: Bool = false
}
在视图中使用时不需要属性包装器:
struct DogCard: View {
let model: DogModel
var body: some View {
VStack {
Text(model.dogs.first?.name ?? "")
Toggle("Favorite", isOn: $model.isFavorite)
}
}
}
关键点:
@Observable宏自动展开为可观察类型- 不需要
@Published,所有存储属性默认可观察 - 视图中直接读取属性,不需要
@ObservedObject - SwiftUI 只追踪实际读取的属性,未读取的属性变化不会触发刷新
- 传递中间视图时不会触发不必要的更新
SwiftData 集成
(13:16)把 @Observable 换成 @Model,模型就获得持久化:
import SwiftData
@Model
class Dog {
var name: String
var breed: String
var spottedDate: Date
init(name: String, breed: String, spottedDate: Date = .now) {
self.name = name
self.breed = breed
self.spottedDate = spottedDate
}
}
在 App 中设置 model container:
@main
struct DogWatcherApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Dog.self)
}
}
在视图中查询数据:
import SwiftData
struct DogList: View {
@Query(sort: \Dog.spottedDate, order: .reverse) private var dogs: [Dog]
var body: some View {
List(dogs) { dog in
Text(dog.name)
}
}
}
关键点:
@Model宏同时提供 Observable 和持久化能力.modelContainer(for:)在场景级别设置存储栈@Query自动从数据库获取数据,数据变化时刷新视图sort参数指定排序规则- 支持过滤、分页等高级查询
Keyframe Animator 动画
(19:07)KeyframeAnimator 支持多属性并行动画:
struct LogoAnimation: View {
@State private var trigger = false
var body: some View {
KeyframeAnimator(
initialValue: AnimationValues(),
trigger: trigger
) { values in
Image("logo")
.offset(y: values.verticalTranslation)
.rotationEffect(values.rotation)
} keyframes: { _ in
KeyframeTrack(\.verticalTranslation) {
SpringKeyframe(30, duration: 0.25, spring: .smooth)
CubicKeyframe(-50, duration: 0.3)
SpringKeyframe(0, spring: .bouncy)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(.degrees(0), duration: 0.25)
CubicKeyframe(.degrees(360), duration: 0.55)
}
}
}
}
struct AnimationValues {
var verticalTranslation: CGFloat = 0
var rotation: Angle = .zero
}
关键点:
initialValue包含所有可动画属性trigger是 Equatable 状态,变化时触发动画- 第一个闭包用动画值构建视图
- 第二个闭包定义每个属性随时间的变化轨迹
KeyframeTrack支持SpringKeyframe、CubicKeyframe、LinearKeyframe- 多个 track 并行执行
Phase Animator 动画
(20:29)PhaseAnimator 按顺序执行一系列动画阶段:
struct WatchButton: View {
@State private var sightingCount = 0
var body: some View {
Button {
sightingCount += 1
} label: {
Image("dog-icon")
.phaseAnimator(
[AnimationPhase.idle, .rotate, .grow, .settle],
trigger: sightingCount
) { content, phase in
content
.rotationEffect(phase.rotation)
.scaleEffect(phase.scale)
} animation: { phase in
switch phase {
case .idle: .default
case .rotate: .spring(.snappy)
case .grow: .spring(duration: 0.3, bounce: 0.5)
case .settle: .spring(.bouncy)
}
}
}
}
}
enum AnimationPhase: CaseIterable {
case idle, rotate, grow, settle
var rotation: Angle {
switch self {
case .idle: .zero
case .rotate: .degrees(15)
case .grow, .settle: .zero
}
}
var scale: CGFloat {
switch self {
case .idle, .rotate: 1.0
case .grow: 1.3
case .settle: 1.0
}
}
}
关键点:
PhaseAnimator按顺序执行阶段,前一阶段完成后才开始下一阶段trigger变化时从头开始执行所有阶段- 每个阶段可以定义不同的动画曲线
- 新的 spring 动画支持
duration和bounce参数 .snappy、.bouncy、.smooth是预设的 spring 配置
ScrollView 改进
(27:53)scrollTransition 给滚动项添加视觉效果:
ScrollView {
LazyVStack {
ForEach(dogs) { dog in
DogCard(dog: dog)
.scrollTransition { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0.5)
.scaleEffect(phase.isIdentity ? 1 : 0.8)
}
}
}
}
关键点:
scrollTransition在项进入/离开可视区域时应用效果phase包含.topLeading、.identity、.bottomTrailing三种状态- 不需要
GeometryReader
分页和对齐:
ScrollView(.horizontal) {
LazyHStack {
ForEach(parks) { park in
ParkCard(park: park)
.containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 16)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
关键点:
containerRelativeFrame让子视图大小相对于容器count把屏幕分成几份,span指定占几份scrollTargetLayout标记需要对齐的布局scrollTargetBehavior(.viewAligned)让滚动停止时对齐到视图边界- 也支持
.paging行为
触觉反馈
(22:03)新的 sensoryFeedback 修饰符:
Button("Record Sighting") {
sightingCount += 1
}
.sensoryFeedback(.success, trigger: sightingCount)
关键点:
.success、.warning、.error等预定义反馈类型trigger变化时播放触觉反馈- 支持所有带触觉反馈的平台
SF Symbol 动画效果
(24:55)symbolEffect 修饰符给 SF Symbol 添加动画:
Image(systemName: "slider.horizontal.3")
.symbolEffect(.bounce, options: .repeat(1))
关键点:
.pulse、.variableColor是持续动画.scale、.appear、.disappear、.replace是状态变化动画.bounce是事件通知动画- 可以应用到单个 symbol 或整个视图层级
核心启发
-
做一个 visionOS 3D 展示应用
- 用
WindowGroup+.volumetric样式展示产品 3D 模型 - 用
Model3D加载 USDZ 文件,用户可以用手势旋转查看 - 入口:
Model3D(named: "product")和.windowStyle(.volumetric)
- 用
-
给现有应用加交互小组件
- 把现有的 Today Widget 升级为支持交互的版本
- 用
Toggle和Button+ App Intents 让用户在小组件上直接操作 - 入口:
#Preview(as: .systemSmall)预览小组件,AppIntent定义操作
-
用 @Observable 重构数据层
- 把现有的
ObservableObject+@Published模型替换为@Observable - 删除大量
@ObservedObject和@EnvironmentObject代码 - 入口:把
class Model: ObservableObject改为@Observable class Model
- 把现有的
-
做一个带动画的 onboarding 流程
- 用
PhaseAnimator做步骤引导动画 - 每个步骤切换时触发不同的 symbol 动画和视图过渡
- 入口:
PhaseAnimator(phases, trigger: step)+symbolEffect
- 用
-
做一个 ScrollView 驱动的画廊
- 横向滚动展示图片,每项占屏幕 80% 宽度
- 用
scrollTransition让边缘项缩小变透明 - 用
scrollTargetBehavior(.viewAligned)实现卡片吸附效果 - 入口:
containerRelativeFrame(.horizontal, count: 5, span: 4)
关联 Session
- Discover Observation in SwiftUI — 深入理解 @Observable 宏的工作原理
- Build an app with SwiftData — 从零开始用 SwiftData 构建应用
- Wind your way through advanced animations in SwiftUI — KeyframeAnimator 和 PhaseAnimator 的完整指南
- Beyond scroll views — ScrollView 滚动过渡、对齐和自定义行为
- Meet SwiftUI for spatial computing — visionOS 上 SwiftUI 的 3D 能力
评论
GitHub Issues · utterances