WWDC Quick Look 💓 By SwiftGGTeam
What's new in SwiftUI

What's new in SwiftUI

观看原视频

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 里可以照常使用 NavigationSplitViewTabView 等容器。

02:26)把场景声明为 .volumetric 样式,就得到一个 Volume。Volume 在有限空间内展示 3D 内容,比如棋盘游戏或建筑模型,和其他应用并排显示。用 Model3D 展示静态模型,用 RealityView 展示带光照和交互的动态模型。

02:59ImmersiveSpace 场景类型定义沉浸式空间体验,支持 mixed 和 full 两种 immersion 样式。mixed 样式把虚拟内容叠加到真实环境,full 样式则让应用完全接管用户的视野。

watchOS 10 的全新设计

04:08)watchOS 10 采用全屏彩色设计。NavigationSplitViewNavigationStack 获得新的转场动画,TabView 新增由数码表冠驱动的垂直分页样式。

04:53containerBackground 修饰符为导航和标签视图配置微妙的背景渐变,在 push 和 pop 时带动画过渡。containerBackground 也支持 TabView

小组件进入更多场景

05:54)小组件出现在更多位置:iPadOS 17 的锁屏、iPhone 的 Standby 模式、macOS Sonoma 的桌面。更关键的是,小组件现在支持交互控件。

06:32ToggleButton 可以通过 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 效果
  • 容器如 NavigationSplitViewTabView 可直接使用

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:07KeyframeAnimator 支持多属性并行动画:

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 支持 SpringKeyframeCubicKeyframeLinearKeyframe
  • 多个 track 并行执行

Phase Animator 动画

20:29PhaseAnimator 按顺序执行一系列动画阶段:

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 动画支持 durationbounce 参数
  • .snappy.bouncy.smooth 是预设的 spring 配置

ScrollView 改进

27:53scrollTransition 给滚动项添加视觉效果:

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:55symbolEffect 修饰符给 SF Symbol 添加动画:

Image(systemName: "slider.horizontal.3")
    .symbolEffect(.bounce, options: .repeat(1))

关键点:

  • .pulse.variableColor 是持续动画
  • .scale.appear.disappear.replace 是状态变化动画
  • .bounce 是事件通知动画
  • 可以应用到单个 symbol 或整个视图层级

核心启发

  1. 做一个 visionOS 3D 展示应用

    • WindowGroup + .volumetric 样式展示产品 3D 模型
    • Model3D 加载 USDZ 文件,用户可以用手势旋转查看
    • 入口:Model3D(named: "product").windowStyle(.volumetric)
  2. 给现有应用加交互小组件

    • 把现有的 Today Widget 升级为支持交互的版本
    • ToggleButton + App Intents 让用户在小组件上直接操作
    • 入口:#Preview(as: .systemSmall) 预览小组件,AppIntent 定义操作
  3. 用 @Observable 重构数据层

    • 把现有的 ObservableObject + @Published 模型替换为 @Observable
    • 删除大量 @ObservedObject@EnvironmentObject 代码
    • 入口:把 class Model: ObservableObject 改为 @Observable class Model
  4. 做一个带动画的 onboarding 流程

    • PhaseAnimator 做步骤引导动画
    • 每个步骤切换时触发不同的 symbol 动画和视图过渡
    • 入口:PhaseAnimator(phases, trigger: step) + symbolEffect
  5. 做一个 ScrollView 驱动的画廊

    • 横向滚动展示图片,每项占屏幕 80% 宽度
    • scrollTransition 让边缘项缩小变透明
    • scrollTargetBehavior(.viewAligned) 实现卡片吸附效果
    • 入口:containerRelativeFrame(.horizontal, count: 5, span: 4)

关联 Session

评论

GitHub Issues · utterances