WWDC22: SwiftUI 的新功能 part 1
SwiftUI 是 Apple 发布于 WWDC 2019 的 UI 框架,采用声明式语法,具有数据绑定、响应式编程及跨平台(Apple 全家桶)等特征。WWDC22 发布了一些新的 API,大大提升了实用性。
Swift Chart
Swift Charts 是 WWDC22 新发布的跨平台图表框架,同 SwiftUI 一样使用声明式语法,很容易自定义。所有的 Chart
对象都是 some View
,所以,它就是一个 SwiftUI 视图组件,支持大部分的 SwiftUI modifier。
var body: some View {
// 作为 `some View` 返回
Chart(partyTasksRemaining) {
LineMark(
x: .value("Date", $0.date, unit: .day),
y: .value("Tasks Remaining", $0.remainingCount)
)
.foregroundStyle(by: .value("Category", $0.category))
}
// 使用 `padding()` 修改器
.padding()
}
也可以结合 SwiftUI 其他组件一起使用
var body: some View {
Chart {
ForEach(partyTasksRemaining) { task in
LineMark(
x: .value("Date", task.date, unit: .day),
y: .value("Tasks Remaining", task.remainingCount)
)
.foregroundStyle(by: .value("Category", task.category))
.symbol(by: .value("Category", task.category))
.annotation(position: .leading) {
// 使用文本标注
Text("\(task.category.emoji)")
}
}
RuleMark(y: .value("Value", 5))
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 2.0, dash: [4, 5]))
.annotation(position: .top, alignment: .trailing) {
// 使用一组文本
VStack(alignment: .trailing) {
Text("Today's Goal")
Text("Status: ✔︎")
}
// 并修饰
.font(.caption)
.foregroundColor(.gray)
.padding(.trailing, 2)
}
}
}
了解 Swift Charts:Swift Charts 初探,再谈 Swift Charts
Navigation and windows
SwiftUI 已经支持常见的导航模式,如 push-and-pop
导航堆栈,分隔窗口和多窗口。WWDC22 对这三种导航模式进行了升级。
NavigationStack
新的容器视图,用来支持 push-and-pop
风格的导航。
向下兼容
NavigationStack {
List(foodItems) { item in
// 支持已有的 `NavigationLink`
NavigationLink {
// 子视图
FoodDetailView(item: item)
} label: {
FoodRow(food: item)
}
}
// 和 `navigationTitle()`
.navigationTitle("Party Food")
}
数据驱动
NavigationStack
提供了新的数据驱动的方式,通过编程控制要展示的内容。新的 navigationDestination()
修改器可以把导航目标与数据类型进行绑定,即根据不同的类型展示对应的 detail view
NavigationStack {
List(foodItems) { item in
// 1. 给定一个 value 用来设定导航目标
NavigationLink(value: item) {
Label(item.title, image: item.iconName)
}
}
.navigationTitle("Party Food")
// 2. 当点击链接,会根据提供的 value 类型判断导航目标
.navigationDesination(for: FooItem.self) { item in
FoodDetailView(item: item)
}
}
想象一下在该示例中添加一下功能:
- 在 food detail view 添加一个相关 food 视图,显示一系列 food item
- 点击相关食物会 push 到新的 detail 页面
- 在任意 food detail view 可以点击一个返回按钮,返回最初选择的 food item
已访问 food item 会存储在一个数组中,用来表示访问路径的堆栈,所以可以直接通过访问和修改该数组来实现_返回_的操作。
struct FoodsListView: View {
fileprivate var foodItems = partyFoods
// 1. 已访问的 food item 存储 state
@State private var selectedFoodItems: [FoodItem] = []
var body: some View {
// 2. 加入导航堆栈
NavigationStack(path: $selectedFoodItems) {
List(foodItems) { item in
NavigationLink(value: item) {
FoodRow(food: item)
}
}
.navigationTitle("Party Food")
.navigationDestination(for: FoodItem.self) { item in
FoodDetailView(item: item, path: $selectedFoodItems)
}
}
}
}
struct FoodDetailView: View {
let item: FoodItem
// 3. 绑定 path
@Binding var path: [FoodItem]
var body: some View {
ScrollView {
VStack {
HStack {
Text(item.emoji)
.font(.system(size: 30))
Text(item.name)
.font(.title3)
}
.padding(.bottom, 4)
Text(item.description)
.font(.caption)
Divider()
RelatedFoodsView(relatedFoods: relatedFoods.random(3, except: item))
if path.count > 1 {
// 4. 点击返回按钮,移除第一个 food item 以外其他项目
Button("Back to First Item") { path.removeSubrange(1...) }
}
}
}
}
}
struct RelatedFoodsView: View {
@State var relatedFoods: [FoodItem]
var body: some View {
VStack {
Text("Related Foods")
.background(.background, in: RoundedRectangle(cornerRadius: 2))
HStack {
ForEach(relatedFoods) { food in
NavigationLink(value: food) {
Text(food.emoji)
}
}
}
}
}
}
NavigationSplitView
分割视图主要用于多列导航中。NavigationSplitView
可以定义两列或三列布局。在较小尺寸下布局 split view 会自动转换为 stack view 布局方式。
// MARK: NavigationSplitView Demo
struct PartyPlannerHome: View {
@State private var selectedTask: PartyTask?
var body: some View {
NavigationSplitView {
// 左侧导航
List(PartyTask.allCases, selection: $selectedTask) { task in
// 使用 `NavigationLink`
NavigationLink(value: task) {
TaskLabel(task: task)
}
.listItemTint(task.color)
}
} detail: {
selectedTask.flatMap { $0.color } ?? .white
}
}
}
struct TaskLabel: View {
let task: PartyTask
var body: some View {
Label {
VStack(alignment: .leading) {
Text(task.name)
Text(task.subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
} icon: {
Image(systemName: task.imageName)
.symbolVariant(.circle.fill)
}
}
}
当需要构建更复杂的布局时,可以把 NavigationSplitView
和 NavigationStack
结合在一起使用
struct PartyPlannerHome: View {
@State private var selectedTask: PartyTask?
var body: some View {
NavigationSplitView {
List(PartyTask.allCases, selection: $selectedTask) { task in
NavigationLink(value: task) {
TaskLabel(task: task)
}
.listItemTint(task.color)
}
} detail: {
if case .food = selectedTask {
// 嵌入一个基于 `NavigationStack` 的视图(self-contained navigation stack),点击 item 会在当前页面更新
FoodsListView()
} else {
selectedTask.flatMap { $0.color } ?? .white
}
}
}
}
Scenes
已有的 WindowGroup
可以用来构建应用的主视图,创建多个交互窗口。WWDC22 SwiftUI 增加了 Window
视图,一个独立的窗口组件。
管理 Window
通常要打开一个 Window
可以通过以下方式:
- 窗口菜单找到
Window
的标题并点击 - 设置快捷键
- 调用 environment action
openWindow
@main
struct PartyPlanner: App {
var body: some Scene {
WindowGroup("Party Planner") {
PartyPlannerHome()
}
// 默认情况下在应用的窗口菜单,点击 Window 标题即可打开。
Window("Party Budget", id: "budget") {
BudgetView()
}
// 或者定义快捷键 `Command+0`
.keyboardShortcut("0")
}
}
使用按钮打开 window
struct DetailView: View {
// 1. 使用 environment aciton
@Environment(\.openWindow) var openWindow
var body: some View {
Text("Detail View")
.toolbar {
Button {
// 2. 点击打开
openWindow(id: "budget")
} label: {
Image(systemName: "dollarsign")
}
}
}
}
自定义 Window
样式
Window
支持默认尺寸、位置、缩放和其他修改器。
@main
struct PartyPlanner: App {
var body: some Scene {
WindowGroup("Party Planner") {
PartyPlannerHome()
}
Window("Party Budget", id: "budget") {
BudgetView()
}
.keyboardShortcut("0")
// 默认位置
.defaultPosition(.topLeading)
// 默认尺寸
.defaultSize(width: 220, height: 250)
}
}
兼容小屏幕
在这个事例中 Window
很适合展示一小组汇总数据。如果要在跨平台时获得更好的体验,就需要对小尺寸屏幕进行一些优化,例如在 iOS 中可以把 Budget view 放在一个 resizable sheet 内。
struct PartyPlannerHome: View {
@State private var selectedTask: PartyTask?
@State private var presented: Bool = false
var body: some View {
NavigationSplitView {
List(PartyTask.allCases, selection: $selectedTask) { task in
NavigationLink(value: task) {
TaskLabel(task: task)
}
.listItemTint(task.color)
}
} detail: {
if case .food = selectedTask {
FoodsListView()
} else {
selectedTask.flatMap { $0.color } ?? .white
}
}
.sheet(isPresented: $presented) {
BudgetView()
// 设置 detents
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
}
}
在 Xcode 内使用 multiplatform targe 发布到多个平台,即可以自动适配。跨平台发布可以查看 WWDC22 的 What's new in Xcode "Use Xcode to develop a multiplatform app" 小节的内容。
Menu bar
在新的 macOS Ventura 系统中,可以创建完全使用 SwiftUI 实现的 MenuBarExtras
。这是一个独立于其他 scene 类型,且在应用运行时会常驻 menu bar 的。
@main
struct PartyPlanner: App {
var body: some Scene {
Window("Party Budget", id: "budget") {
Text("Budget View")
}
// 2. 为 App 添加一个 MenuBarExtra
MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
BulletinBoard()
}
// 3. 定义样式
.menuBarExtraStyle(.window)
}
}
private let allPosts: [String] = [
"Did you know: On your third birthday, you are celebrating your 4.0 release.",
]
// 1. 创建一个用于 menu bar extra 的视图
struct BulletinBoard: View {
@State var currentPostIndex: Int = 0
var currentPost: String {
allPosts[currentPostIndex]
}
var body: some View {
VStack(spacing: 16) {
VStack(spacing: 12) {
HStack(alignment: .firstTextBaseline) {
Text("“")
.font(.custom("Helvetica", size: 50).bold())
.baselineOffset(-23)
.foregroundStyle(.tertiary)
Text("Party Bulletin Board")
.font(.headline.weight(.semibold))
.foregroundStyle(.secondary)
Spacer()
Text("June 6, 2022")
.font(.headline.weight(.regular))
.foregroundStyle(.secondary)
}
.frame(height: 20)
Text(currentPost)
.font(.system(size: 18))
.multilineTextAlignment(.center)
}
.padding(.bottom, 4)
Divider()
HStack {
Button {
} label: {
Label("Calendar", systemImage: "calendar")
}
Button {
currentPostIndex = (currentPostIndex + 1) % allPosts.count
} label: {
Text("Previous")
.frame(maxWidth: .infinity)
}
ShareLink(items: [currentPost])
}
.labelStyle(.iconOnly)
.controlSize(.large)
}
.padding(16)
}
}
甚至可以只使用 MenuBarExtras
构建一个应用。
@main
struct MessageBoard: App {
var body: some Scene {
MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
BulletinBoard()
}
.menuBarExtraStyle(.window)
}
}
要想了解更多关于新的 scene 类型和功能,可以访问 WWDC22 的 Bring Multiple Windows to Your SwiftUI App。