WWDC Quick Look 💓 By SwiftGGTeam
Introduction to SwiftUI

Introduction to SwiftUI

观看原视频

Highlight

Apple 用 Sandwiches 示例从零构建一个 iOS、iPadOS 和 macOS 通用 SwiftUI App,展示 View 组合、source of truth、@State@StateObject、列表编辑、动画和 Xcode 预览如何串成声明式开发流程。

核心内容

很多 UI 入门教程会从控件清单开始。这个 session 的做法更直接:打开 Xcode,新建一个多平台项目,然后把一个记录三明治的 App 一步步做出来。开发者先看到代码和 canvas 同步更新,再看到列表、导航、详情页、状态、动画、数据模型和预览如何接上同一条线。

传统 UI 开发里,视图对象通常承担太多职责。开发者要创建对象、设置属性、响应事件、维护导航和列表状态,还要手动处理刷新时机。SwiftUI 的切入点是把界面写成值:body 描述当前数据应该呈现出的界面,框架在数据变化后重新取一份新的描述。

Sandwiches 示例正好覆盖一个真实 App 的基本骨架。列表从硬编码单元格开始,接入 Identifiable 模型后变成数据驱动列表;详情页用 @State 控制图片缩放;根数据从静态数组升级成 ObservableObject;最后再用预览检查深色模式、动态字体、阿拉伯语和从右到左布局。

这场 session 不是 2020 年所有 SwiftUI 新 API 的总览。它更像一条入门路径:先理解 View 很轻,放心拆小组件;再理解 source of truth,知道状态应该放在哪里;最后把 Xcode 预览当作开发循环的一部分,而不是运行 App 之后的补救工具。

详细内容

View 是轻量的值

17:18)Kyle 在中段把演示暂停下来,解释 SwiftUI View 的模型。View 是符合 View 协议的 struct,不继承 UIView 这样的基类,也不拥有一串框架分配的存储。框架会把 view hierarchy 折叠成适合渲染的数据结构,所以开发者可以把界面拆成小的单用途 View。

struct SandwichDetail: View {
    let sandwich: Sandwich

    var body: some View {
        Image(sandwich.imageName)
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}

关键点:

  • SandwichDetail 是一个 struct,存储的输入只有 sandwich
  • bodyView 协议唯一要求的属性,负责返回界面描述。
  • Image(sandwich.imageName) 从模型里的图片名创建图片视图。
  • .resizable().aspectRatio(contentMode: .fit) 都是修饰器,它们返回新的 View 描述。
  • 这段代码来自官方 snippet,依赖 session 前面导入的 Sandwich 模型。

View 通过组合变大

18:30)SwiftUI 的组合方式和 demo 前半段一致:VStack 垂直排文字,HStack 水平排图片和文字,List 包住多行,NavigationLink 再把单元格接到详情页。SandwichDetail 里也能看到同样的模式,图片、缩放和比例控制被组合成一个新的 View。

struct SandwichDetail: View {
    let sandwich: Sandwich

    var body: some View {
        Image(sandwich.imageName)
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}

关键点:

  • ImageresizableaspectRatio 在渲染时合成一个详情页图片区域。
  • 修饰器参与构造新的 View 描述,不走事后修改已有对象的路径。
  • body 被重新读取时,SwiftUI 会拿到一份新的描述,再决定哪些渲染需要更新。
  • 这种模型让 “Extract Subview” 变成低成本重构,session 中把列表 cell 拆成 SandwichCell 就基于这个前提。

@State 让状态成为 source of truth

19:52)详情页需要在 “fit” 和 “fill” 之间切换图片。SwiftUI 的做法是声明一个状态变量 zoomed,再让 aspectRatiocontentMode 从这个状态派生出来。点击手势只修改状态,界面刷新由框架完成。

struct SandwichDetail: View {
    let sandwich: Sandwich
    @State private var zoomed = false

    var body: some View {
        Image(sandwich.imageName)
            .resizable()
            .aspectRatio(contentMode: zoomed ? .fill : .fit)
            .onTapGesture { zoomed.toggle() }
    }
}

关键点:

  • @State private var zoomed = false 是这个 View 自己拥有的 source of truth。
  • contentMode 是派生值,它由 zoomed 决定。
  • .onTapGesture 只调用 zoomed.toggle(),不直接操作图片对象。
  • SwiftUI 观察到 body 读取过 zoomed,所以 zoomed 改变后会重新请求 body
  • 这解释了 session 里说的 “每个可能的 UI 状态都来自权威数据”。

动画和条件视图仍然由数据驱动

31:31)图片缩放后,demo 又加了 ignoresSafeAreawithAnimationLabelSpacer、条件渲染和 .transition(.move(edge: .bottom))。重点不在某个控件,而在同一条规则:改变状态,SwiftUI 根据新状态插入、移除或移动 View。

Image(sandwich.imageName)
    .resizable()
    .aspectRatio(contentMode: zoomed ? .fill : .fit)
    .ignoresSafeArea(edges: .bottom)
    .onTapGesture {
        withAnimation {
            zoomed.toggle()
        }
    }

关键点:

  • .ignoresSafeArea(edges: .bottom) 对应 demo 中让图片扩展到底部安全区之外的步骤。
  • withAnimation 包住状态变化,动画和最终状态绑定在一起。
  • 如果用户在动画过程中再次点击,新的状态仍然是 zoomed 的结果,框架会让动画反向或继续。
  • 后面隐藏 spicy banner 时,条件判断只检查 sandwich.isSpicyzoomed,不需要手写插入、删除视图的生命周期。

App 级数据用 ObservableObject 接入

42:55)列表最初由静态数组驱动。为了支持新增、删除和排序,session 把数据升级为一个可变 store:store 符合 ObservableObject,会变化的 sandwiches 标成 @Published。App 入口用 @StateObject 拥有 store,列表 View 用 @ObservedObject 观察它。

final class SandwichStore: ObservableObject {
    @Published var sandwiches: [Sandwich] = []
}

@main
struct SandwichesApp: App {
    @StateObject private var store = SandwichStore()

    var body: some Scene {
        WindowGroup {
            SandwichList(store: store)
        }
    }
}

struct SandwichList: View {
    @ObservedObject var store: SandwichStore

    var body: some View {
        List {
            ForEach(store.sandwiches) { sandwich in
                SandwichCell(sandwich: sandwich)
            }
            .onMove(perform: moveSandwiches)
            .onDelete(perform: deleteSandwiches)
        }
    }
}

关键点:

  • ObservableObject 让模型对象能通知 SwiftUI 数据即将变化。
  • @Published 标记需要被观察的属性,session 中用它包装三明治数组。
  • @StateObject 放在 App 入口,适合拥有 App 级 store。
  • @ObservedObject 放在列表 View,表示这个 View 使用外部传入的 store。
  • ForEach 依赖 Sandwich 符合 Identifiable,这样插入、删除、移动时 SwiftUI 可以合成正确的列表更新。

预览也是 SwiftUI 代码

49:36)结尾部分展示了预览的价值。开发者可以复制一份预览,把 Dynamic Type 调大;再复制一份设置深色模式;再设置从右到左布局和阿拉伯语 locale。Xcode 添加的内容仍然是 SwiftUI 修饰器。

SandwichList(store: .test)
    .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)

SandwichList(store: .test)
    .environment(\.colorScheme, .dark)

SandwichList(store: .test)
    .environment(\.layoutDirection, .rightToLeft)
    .environment(\.locale, Locale(identifier: "ar"))

关键点:

  • .environment 把上下文信息注入到整个 View 层级。
  • Dynamic Type、深色模式、布局方向和 locale 都能在预览里单独覆盖。
  • Text 使用字符串字面量时,SwiftUI 默认把它当作可本地化文本。
  • 模型里的字符串值会按原值展示,适合标题、名称这类来自数据源的内容。
  • session 最后强调,整个 Sandwiches App 的许多行为都在预览里完成验证,没有先完整 build-and-run。

核心启发

1. 做一个可预览的详情页缩放交互。做什么:给图片详情页加点击缩放和退出缩放。为什么值得做:session 已经用 @StateaspectRatiowithAnimation 证明这类交互可以只围绕一个布尔状态展开。怎么开始:先把图片详情页写成独立 View,声明 @State private var zoomed = false,再让 contentMode 和 banner 是否显示都从 zoomed 派生。

2. 做一个跨平台清单 App 原型。做什么:用同一套 SwiftUI 列表和详情页跑在 iPhone、iPad 和 macOS 上。为什么值得做:demo 里 NavigationView 在 iPhone 上是 push,在 iPad 和 macOS 上能变成分栏。怎么开始:用多平台模板创建项目,把共享模型放在 shared group,用列表作为第一栏,再给 NavigationView 添加占位详情视图。

3. 做一个可编辑的数据列表。做什么:让用户新增、删除、重排项目。为什么值得做:session 用 ObservableObject@PublishedForEachonMoveonDelete 演示了完整数据流。怎么开始:先让模型符合 Identifiable,再把数组放进 store,由 App 入口用 @StateObject 持有,列表通过 @ObservedObject 读取并调用修改方法。

4. 做一组极端环境预览。做什么:在同一份 preview 里覆盖大字号、深色模式、从右到左布局和目标语言。为什么值得做:结尾演示说明这些系统行为可以在 canvas 中快速验证。怎么开始:给关键页面创建多份 preview,分别添加 .environment(\.sizeCategory, ...).environment(\.colorScheme, .dark).environment(\.layoutDirection, .rightToLeft).environment(\.locale, ...)

5. 做组件级重构练习。做什么:把大型列表 cell、详情 header、底部状态 banner 拆成独立 View。为什么值得做:session 明确说明 SwiftUI View 很轻,抽取子视图几乎没有运行时负担。怎么开始:先用 Xcode 的 “Extract Subview” 拆出 SandwichCell,再用输入属性接收模型,把状态留在真正拥有它的父级或当前 View。

关联 Session

评论

GitHub Issues · utterances