大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftUI 中的检查器:探索细节之美
检查器是一种结构化的 API,可以为你的 App 带来更多细节。我们将带你了解该API的基础知识,并向你展示如何使用它。了解有关自定义表单呈现的最新更新,并学习如何将其与检查器结合以打造完美的视图呈现体验。
章节
- 0:32 - Inspector
- 3:27 - 🍎🍐🍋🍒
- 9:11 - 🍏
- 9:37 - Presentation customizations
资源
相关视频
WWDC23
-
下载
♪ ♪
Nick:大家好 我是 Nick 我是 SwiftUI 团队的工程师 检查器是 SwiftUI 中 一个激动人心的新元素 我将介绍检查器是什么 以及该如何使用其 API 之后 我将介绍用于 自定义视图呈现的修饰符 检查器视图可以显示 所选内容的更多详细信息 你之前可能就使用过检查器 Keynote 讲演就使用检查器 来显示所选内容的 格式详细信息 在这里是形状编辑器 这里 检查器在右侧 以侧边栏的形式呈现 检查器的另一个常见用途 是用来显示 App 主要内容的 补充内容 “快捷指令”中的检查器 就是提供了这个功能 它的主界面 显示用户正在编辑的快捷指令 而检查器通过列出 可用的 App 和操作来补充其功能 我将使用这个演示 App 来介绍该 API 我最近一直在了解 Apple Park 里和附近的动物 这个 App 可以记录我见过的动物 包括它们的名字、 它们最喜欢的水果 还有这一栏 叫做“可疑程度” 我之后会介绍 现在让我们来认识一下 SwiftUI 的检查器吧! 检查器会显示所选动物的 详细信息的读写视图 在这里 我将 Fabrizio Fish 的可疑程度 提高到“极其可疑” 我还是把它标为“可疑”好了 检查器可供 SwiftUI 开发者在 macOS、 iPadOS 以及 iOS 上使用 检查器 API 包含 对列宽的编程控制 可以让开发者调整右侧边栏的宽度 该 API 还包含 对视图展示状态的编程控制 可以让你根据需要 展示或隐藏检查器 检查器可不只是单纯的侧边栏 它还浓缩了更高级的内容 在紧凑尺寸类别中 它还能适应可调整尺寸的表单 而在更大尺寸的 iPad 上 调整器可以自动叠加显示在分屏上 SwiftUI 已经具备一套 现有的结构 API 而检查器与这些 API 是同类 它们都具有导航组件和演示的特性 与 NavigationSplitView 和 NavigationStack 类似 检查器可以用于构建场景的框架 与表单、弹窗、警报 以及确认对话框类似 检查器也是一种视图演示方式 可以根据需要取消或展示 那么让我们来学习 如何使用新的检查器 API 吧 之前我介绍了我为这次讲座 制作的演示 App 你可能注意到了 我在调查每只动物的可疑程度 那是因为我正在尝试 解开一个高风险的谜团 有人正在吃掉 Apple Park 所有的水果! 我为我的 App 添加了检查器 这样的话 本人 检察官 Nick 就可以收集每只动物的详细信息 我将向你展示 采用检查器 API 有多么简单 或许 我还能在过程中 解决这个有关水果的谜团 让我们快点打开 Xcode! 水果游戏开始了! 添加检查器的第一步 是使用新的修饰符 inspector 与其他视图呈现类似 我们需要 使用一个 Bool 类型的绑定 然后是右侧的视图构建器中的 检查器内容 对于其内容 我已经准备好了一个 AnimalInspectorForm 我之前自定义了一些方法 用来添加我目前在调查的动物 这就是我的调查器 在 macOS 上作为右侧边栏出现 我已经为我出色的调查体验 打好了基础 这个 AnimalForm 使用了分组样式 如果你之前没有遇到过表样式 你可以这样应用它们 但是由于检查器默认使用分组样式 所以我不需要自己设置样式 我甚至可以在预览界面中 与检查器进行交互 啊 正好提醒我了 尽管检查器默认可以折叠 但默认情况下它们不能调整尺寸 我可以通过检查器的 列宽修改器来改变其尺寸 我会设置一些合理的默认值 最小宽度设为 200 理想宽度 300 最大 400 这个理想参数将会是 首次启动时的列宽 但如果用户重新 调整了检查器的尺寸 系统将在后续的启动中 继续使用该尺寸 最后 我会添加一个工具栏项 来切换显示状态 我会使用一个按钮来切换显示属性 并为其标签使用一个 Label 使用 info.circle 系统图像 工具栏项将出现在检查器上方的 工具栏部分中 因为它是 在检查器的视图构建器中声明的 当我滚动时 工具栏的表现与我预期的完全相同 当工具栏下方有足够多的内容时 在工具栏的下缘显示阴影
我特意在 AnimalTable 上 使用了检查器修饰符 而不是在 视图层次结构中的其他位置 与许多 SwiftUI API 一样 检查器修饰符 会根据其应用的位置有不同的表现 具体来说 修饰符的位置决定了 是使用全高样式 即工具栏 和内容之间没有分隔 还是使用下方工具栏样式 即检查器会嵌套在工具栏下方 请注意 在下方工具栏样式中 标题分隔栏跨越了整个窗口的宽度 同样地 工具栏内容 可以放在主内容的工具栏中…… 或者放在检查器工具栏中 具体要取决于 工具栏修饰符的使用位置 我们在使用检查器 API 时 有两个要考虑的问题 首先 检查器要放在 导航结构 比如 NavigationStack NavigationSplitView 的 内部还是外部? 其次 工具栏内容要放在 检查器视图构建器的 内部还是外部? 我将介绍其中两种构造 首先是将检查器 放在导航结构的内部 而工具栏内容放在检查器之外 当检查器包含在 NavigationStack 内部时 检查器会位于 导航堆栈的工具栏下方 在这里 我将工具栏内容 声明在主要内容的 检查器外部 使其呈现在导航堆栈的工具栏中 在紧凑水平尺寸类别中 检查器会呈现为一个表单 而工具栏项则保留在 主内容的工具栏中 让我们回到之前的可能性图表 对于第二种构造 我将检查器放在导航结构外部 而将工具栏内容 放在检查器视图构建器内部 当检查器修饰符 放在导航结构之外时 检查器将使用 右侧边栏的全高进行呈现 如果检查器有工具栏内容 该内容将出现在检查器 特定的工具栏分区 这两个工具栏项的位置 是根据主要位置确定的 并且最终会居中显示在 检查器上方的导航工具栏中 然而这一次 由于工具栏内容 位于检查器视图构建器的内部 当检查器以表单的形式呈现时 工具栏内容也出现在表单内
这些原则同样适用于 macOS 但在 macOS 上 检查器不会呈现为表单 所以其构造方式会更简单 我们唯一需要关注的变量 就是导航结构内部和外部 最后但同样重要的一点是: 如果你在 NavigationSplitView 中使用检查器 检查器应该放在 详情栏的视图构建器中 或者 就像以前一样 你也可以把它 完全放在导航结构之外 哇哦 解开这么多谜题之后我都饿了 哦! 我在 App 上收到了 Nibble Bulletin! 我的 App 上的 Nibble Bulletin 用可以调整尺寸的表单 来呈现何时何地有水果被吃掉了 正好我可以介绍一下 SwiftUI 在 iOS 16.4 中发布的 自定义视图呈现功能 “水果被咬快报”是一个表单 而不是检查器 演示修饰符可以让开发者对表单 和弹窗等演示进行深度定制 我将演示其中几个修饰符 演示背景修饰符正如其名 我们可以用它设置演示的背景 与现有的背景修饰符不同 这个专门用于演示的修饰符 将填充整个视图 并可以让底层内容透出来 那么 如果我使用 thinMaterial 我可以看到后面的列表 从表单中轻微透过来 我希望我在看 Nibble Bulletin 时 可以与表单后面的内容进行交互 以便我可以滚动列表并查看嫌疑人 只要启用 presentationBackgroundInteraction 即可轻松实现 暗调视图已经被移除了 现在我可以与背景内容进行交互 PresentationBackgroundInteraction 类型 还具有 upThrough 参数 可用于规定视图的限制位置 只要提供的参数 与规定的视图限位之一匹配 SwiftUI 就会在视图位置高于 upThrough 参数限制时 调暗其背景 我希望背景能在 视图高度大于 200 之后变暗 我先用 presentationDetents 修饰符 添加一个 200 的高度限制 我将表单从本来的半高限位 调低至我自定义的 200 当我仅允许在高度 200 以下 进行背景交互时 当表单处于半高或全高限位时 背景就又会变暗 开发者可以选择更多自定义选项 来打造完美的视图呈现体验 其中许多修饰符 也会对其他视图产生影响 不仅是表单 关于演示修饰符还有一个要点 当检查器以表单形式呈现时 这些修饰符可以与检查器组合使用 对于我之前的检查器 如果要在半高以上禁用背景交互 我可以使用 和之前一模一样的代码…… 声明高度限位 然后只允许 表单在限高之内时进行背景交互 现在 背景就会在 检查器高度超过半高时变暗了 以上就是 SwiftUI 中检查器的内容 在本视频中 我介绍了检查器 API 和其使用上的细微差别 我挑选了几个我最爱的 表单演示修饰符来演示 并介绍了 如何与检查器组合使用它们 那么 你还在等什么? 快开始用检查器吧 将检查器添加进你的 App 中 通过自定义你的视图呈现 让它更出色 至于偷吃水果的到底是谁 我只能说我们这次的调查 并没有什么成“果” ♪ ♪
-
-
3:35 - Sample models and views
// Copy+Paste the below into an Xcode project to support building and running the session's code snippets import SwiftUI @main struct SwiftUIInspectors: App { var body: some Scene { WindowGroup { ContentView() .environmentObject(AnimalStore()) } } } struct AnimalInspectorForm: View { var animal: Binding<Animal>? @EnvironmentObject private var animalStore: AnimalStore var body: some View { Form { if let animal = animal { SelectedAnimalInspector(animal: animal, animalStore: animalStore) } else { ContentUnavailableView { Image(systemName: "magnifyingglass.circle") } description: { Text("Select a suspect to inspect") } actions: { Text("Fill out details from the interview") } } } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } } struct SelectedAnimalInspector: View { @Binding var animal: Animal @ObservedObject var animalStore: AnimalStore var body: some View { Section("Identity") { TextField("Name", text: $animal.name) Picker("Paw Size", selection: $animal.pawSize) { Text("Small").tag(PawSize.small) Text("Medium").tag(PawSize.medium) Text("Large").tag(PawSize.large) } FruitList(selectedFruits: $animal.favoriteFruits, fruits: allFruits) } Section { TextField(text: animalStore(\.alibi, for: animal), prompt: Text("What was \(animal.name) doing at the time of nibbling?"), axis: .vertical) { Text("Alibi") } .lineLimit(4, reservesSpace: true) if let schedule = Binding(animalStore(\.sleepSchedule, for: animal)) { SleepScheduleView(schedule: schedule) } else { Button("Add Sleep Schedule") { animalStore.write(\.sleepSchedule, value: Animal.Storage.newSleepSchedule, for: animal) } } Slider( value: animalStore(\.suspiciousLevel, for: animal), in: 0...1) { Text("Suspicion Level") } minimumValueLabel: { Image(systemName: "questionmark") } maximumValueLabel: { Image(systemName: "exclamationmark.3") } } header: { Text("Interview") } .presentationDetents([.medium, .large]) } } private struct FruitList: View { @Binding var selectedFruits: [Fruit] var fruits: [Fruit] var body: some View { Section("Favorite Fruits") { ForEach(allFruits) { fruit in Toggle(isOn: .init(get: { selectedFruits.contains(fruit) }, set: { newValue in if newValue && !selectedFruits.contains(fruit) { selectedFruits.append(fruit) } else { _ = selectedFruits.firstIndex(of: fruit).map { selectedFruits.remove(at: $0) } } })) { HStack { FruitImage(fruit: fruit, size: .init(width: 40, height: 40), bordered: true) Text(fruit.name).font(.body) } } } } } @ViewBuilder private func selectionBackground(isSelected: Bool) -> some View { if isSelected { RoundedRectangle(cornerRadius: 2).inset(by: -2) .fill(.selection) } } } private struct SleepScheduleView: View { @Binding var schedule: Animal.Storage.SleepSchedule var body: some View { DatePicker(selection: .init(get: { Calendar.current.date(from: schedule.sleepTime) ?? Date() }, set: { newDate in schedule.sleepTime = Calendar.current.dateComponents([.hour, .minute], from: newDate) }), displayedComponents: [.hourAndMinute]) { Text("Sleep at: ") } DatePicker(selection: .init(get: { Calendar.current.date(from: schedule.wakeTime) ?? Date() }, set: { newDate in schedule.wakeTime = Calendar.current.dateComponents([.hour, .minute], from: newDate) }), displayedComponents: [.hourAndMinute]) { Text("Awake at: ") } } } struct AppState { var selection: String? = "Snail" var animals: [Animal] = allAnimals var inspectorPresented: Bool = true var inspectorWidth: CGFloat = 270 var cornerRadius: CGFloat? = nil } extension Binding where Value == AppState { func binding() -> Binding<Animal>? { self.projectedValue.animals.first { $0.wrappedValue.id == self.selection.wrappedValue } } } extension Animal { struct Storage: Codable { var alibi: String = "" var sleepSchedule: SleepSchedule? = nil /// Value between 0 and 1 representing how suspicious the animal is. /// 1 is guilty. var suspiciousLevel: Double = 0.0 struct SleepSchedule: Codable { var sleepTime: DateComponents var wakeTime: DateComponents } static let newSleepSchedule: SleepSchedule = { // Asleep at 10:30, awake at 6:30 .init( sleepTime: DateComponents(hour: 22, minute: 30), wakeTime: DateComponents(hour: 6, minute: 30)) }() } } final class AnimalStore: ObservableObject { var storage: [Animal.ID: Animal.Storage] = [:] /// Getter for properties of an animal stored in self func callAsFunction<Result>(_ keyPath: WritableKeyPath<Animal.Storage, Result>, for animal: Animal) -> Binding<Result> { Binding { [self] in storage[animal.id, default: .init()][keyPath: keyPath] } set: { [self] newValue in self.objectWillChange.send() var animalStore = storage[animal.id, default: .init()] animalStore[keyPath: keyPath] = newValue storage[animal.id] = animalStore } } func write<Value>(_ keyPath: WritableKeyPath<Animal.Storage, Value>, value: Value, for animal: Animal) { objectWillChange.send() var animalStore = storage[animal.id, default: .init()] animalStore[keyPath: keyPath] = value storage[animal.id] = animalStore } func read<Value>(_ keyPath: WritableKeyPath<Animal.Storage, Value>, for animal: Animal) -> Value { storage[animal.id, default: .init()][keyPath: keyPath] } } struct AnimalTable: View { @Binding var state: AppState @EnvironmentObject private var animalStore: AnimalStore @Environment(\.horizontalSizeClass) private var sizeClass: UserInterfaceSizeClass? var fruitWidth: CGFloat { #if os(iOS) 40.0 #else 25.0 #endif } var body: some View { Table(state.animals, selection: $state.selection) { TableColumn("Name") { animal in HStack { Text(animal.emoji).font(.title) .padding(2) .background(.thickMaterial, in: RoundedRectangle(cornerRadius: 3)) Text(animal.name + " " + animal.species).font(.title3) } } TableColumn("Favorite Fruits") { animal in HStack { ForEach(animal.favoriteFruits.prefix(3)) { fruit in FruitImage(fruit: fruit, size: .init(width: fruitWidth, height: fruitWidth), scale: 2.0, bordered: state.selection == animal.id) } } .padding(3.5) } TableColumn("Suspicion Level") { animal in SuspicionTableCell(animal: animal) } } #if os(macOS) .alternatingRowBackgrounds(.disabled) #endif .tableStyle(.inset) } } private struct SuspicionTableCell: View { var animal: Animal @Environment(\.backgroundProminence) private var backgroundProminence @EnvironmentObject private var animalStore: AnimalStore var body: some View { let color = SuspiciousText.model(for: animalStore.read(\.suspiciousLevel, for: animal)).1 HStack { Image( systemName: "cellularbars", variableValue: animalStore.read(\.suspiciousLevel, for: animal) ) .symbolRenderingMode(.hierarchical) SuspiciousText( suspiciousLevel: animalStore.read(\.suspiciousLevel, for: animal), selected: backgroundProminence == .increased) } .foregroundStyle(backgroundProminence == .increased ? AnyShapeStyle(.white) : AnyShapeStyle(color)) } } private struct SuspiciousText: View { var suspiciousLevel: Double var selected: Bool static fileprivate func model(for level: Double) -> (String, Color) { switch level { case 0..<0.2: return ("Unlikely", .green) case 0.2..<0.5: return ("Fishy", .mint) case 0.5..<0.9: return ("Very suspicious", .orange) case 0.9...1: return ("Extremely suspicious!", .red) default: return ("Suspiciously Unsuspicious", .blue) } } var body: some View { let model = Self.model(for: suspiciousLevel) Text(model.0) .font(.callout) } } struct Animal: Identifiable { var name: String var species: String var pawSize: PawSize var favoriteFruits: [Fruit] var emoji: String var id: String { species } } var allAnimals: [Animal] = [ .init(name: "Fabrizio", species: "Fish", pawSize: .small, favoriteFruits: [.arbutusUnedo, .bigBerry, .elstar], emoji: "🐟"), .init(name: "Soloman", species: "Snail", pawSize: .small, favoriteFruits: [.elstar, .flavorKing], emoji: "🐌"), .init(name: "Ding", species: "Dove", pawSize: .small, favoriteFruits: [.quercusTomentella, .pinkPearlApple, .lapins], emoji: "🕊️"), .init(name: "Catie", species: "Crow", pawSize: .small, favoriteFruits: [.pinkPearlApple, .goldenNectar, .hauerPippin], emoji: "🐦⬛"), .init(name: "Miko", species: "Cat", pawSize: .small, favoriteFruits: [.belleDeBoskoop, .tompkinsKing, .lapins], emoji: "🐈"), .init(name: "Ricardo", species: "Rabbit", pawSize: .small, favoriteFruits: [.mariposa, .elephantHeart], emoji: "🐰"), .init(name: "Cornelius", species: "Duck", pawSize: .medium, favoriteFruits: [.greenGage, .goldenNectar], emoji: "🦆"), .init(name: "Maria", species: "Mouse", pawSize: .small, favoriteFruits: [.arbutusUnedo, .elephantHeart], emoji: "🐹"), .init(name: "Haku", species: "Hedgehog", pawSize: .small, favoriteFruits: [.christmasBerry, .creepingSnowberry, .goldenGem], emoji: "🦔"), .init(name: "Rénard", species: "Raccoon", pawSize: .medium, favoriteFruits: [.belleDeBoskoop, .bigBerry, .christmasBerry, .kakiFuyu], emoji: "🦝") ] enum PawSize: Hashable { case small case medium case large } struct Fruit: Identifiable, Hashable { var name: String var color: Color var id: String { name } } struct FruitImage: View { var fruit: Fruit var size: CGSize? = .init(width: 50, height: 50) var scale: CGFloat = 1.0 var bordered = false var body: some View { fruit.color // Actual assets replaced with Color .scaleEffect(scale) .scaledToFill() .frame(width: size?.width, height: size?.height) .mask { RoundedRectangle(cornerRadius: 4) } .overlay { if bordered { RoundedRectangle(cornerRadius: 4) .stroke(fruit.color, lineWidth: 2) } } } } extension Fruit { static let goldenGem = Fruit(name: "Golden Gem Apple", color: .yellow) static let flavorKing = Fruit(name: "Flavor King Plum", color: .purple) static let mariposa = Fruit(name: "Mariposa Plum", color: .red) static let tompkinsKing = Fruit(name: "Tompkins King Apple", color: .yellow) static let greenGage = Fruit(name: "Green Gage Plum", color: .green) static let lapins = Fruit(name: "Lapins Sweet Cherry", color: .purple) static let hauerPippin = Fruit(name: "Hauer Pippin Apple", color: .red) static let belleDeBoskoop = Fruit(name: "Belle De Boskoop Apple", color: .red) static let elstar = Fruit(name: "Elstar Apple", color: .yellow) static let goldenDeliciousApple = Fruit(name: "Golden Delicious Apple", color: .yellow) static let creepingSnowberry = Fruit(name: "Creeping Snowberry", color: .white) static let quercusTomentella = Fruit(name: "Channel Island Oak Acorn", color: .brown) static let elephantHeart = Fruit(name: "Elephant Heart Plum", color: .red) static let goldenNectar = Fruit(name: "Golden Nectar Plum", color: .yellow) static let pinkPearlApple = Fruit(name: "Pink Pearl Apple", color: .pink) static let christmasBerry = Fruit(name: "Christmas Berry", color: .red) static let kakiFuyu = Fruit(name: "Kaki Fuyu Persimmon", color: .orange) static let bigBerry = Fruit(name: "Big Berry Manzanita", color: .red) static let arbutusUnedo = Fruit(name: "Strawberry Tree", color: .red) } extension Array where Element == Fruit { var groupID: Fruit.ID { reduce("") { result, next in result.appending(next.id) } } } var allFruits: [Fruit] = [ .goldenGem, .flavorKing, .mariposa, .tompkinsKing, .greenGage, .lapins, .hauerPippin, .belleDeBoskoop, .elstar, .goldenDeliciousApple, .creepingSnowberry, .quercusTomentella, .elephantHeart, .goldenNectar, .kakiFuyu, .bigBerry, .arbutusUnedo, .pinkPearlApple, ]
-
3:54 - Xcode Previews
import SwiftUI #Preview("Meet Inspector", traits: .fixedLayout(width: 800, height: 500) ) { ContentView() .navigationTitle("SwiftUI Inspectors") .environmentObject(AnimalStore()) } public struct ContentView: View { @State private var state = AppState() @State private var presented = true public var body: some View { AnimalTable(state: $state) .inspector(isPresented: $presented) { AnimalInspectorForm(animal: $state.binding()) .inspectorColumnWidth( min: 200, ideal: 300, max: 400) .toolbar { Spacer() Button { presented.toggle() } label: { Label("Toggle Inspector", systemImage: "info.circle") } } } } } import MapKit struct FruitNibbleBulletin: View { var fruit: Fruit = .pinkPearlApple @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { ScrollView { VStack(alignment: .leading) { Grid(horizontalSpacing: 12, verticalSpacing: 2) { GridRow { FruitImage(fruit: fruit, size: .init(width: 60, height: 60), bordered: false) Text(""" A \(fruit.name.lowercased()) was nibbled! The bite \ happened at 9:41 AM. The nibbler left behind only \ a few seeds. """ ) } GridRow { Text(""" The Fruit Inspectors were on \ the scene moments after it happened. \ Unfortunately, their efforts to catch the nibbler \ were fruitless. """).gridCellColumns(2) } } GroupBox("Clues") { LabeledContent("Paw Size") { Text("Large") } LabeledContent("Favorite Fruit") { Text("\(fruit.name.capitalized(with: .current))") } LabeledContent("Alibi") { Text("None") } } HStack { VStack { fruit.color .aspectRatio(contentMode: ContentMode.fit) .shadow(radius: 2.5) Text("The pink pearls left behind").font(.caption) .frame(alignment: .leading) } AppleParkMap() .mask(RoundedRectangle(cornerSize: CGSize(width: 20, height: 10))) } Text("The Fruit Inspection team was on the scene minutes after the incident. However, their attempts to discover any meaningful clues around the identity of the nibbler were fruitless.") } .scenePadding(.horizontal) .toolbar { ToolbarItem { Button(role: .cancel) { dismiss() } label: { Label("Close", systemImage: "xmark.circle.fill") } .symbolRenderingMode(.monochrome) .tint(.secondary) } } } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .navigationTitle("Fruit Nibble Bulletin") } } } struct AppleParkMap: View { @State private var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.334_371, longitude: -122.009_558), latitudinalMeters: 100, longitudinalMeters: 100 ) var body: some View { GeometryReader { geometry in Map(position: .constant(.automatic), bounds: .init(centerCoordinateBounds: region, minimumDistance: 100, maximumDistance: 100), interactionModes: [], scope: .none) { } } .frame(height: 180, alignment: .center) } }
-
7:14 - Inspector content inside a navigation structure
struct Example1: View { @State private var state = AppState() var body: some View { NavigationStack { AnimalTable(state: $state) .inspector(isPresented: $state.inspectorPresented) { AnimalInspectorForm(animal: $state.binding()) } .toolbar { Button { state.inspectorPresented.toggle() } label: { Label("Toggle Inspector", systemImage: "info.circle") } } } } }
-
7:55 - Inspector content outside a navigation structure
struct Example2: View { @State private var state = AppState() var body: some View { NavigationStack { AnimalTable(state: $state) } .inspector(isPresented: $state.inspectorPresented) { AnimalInspectorForm(animal: $state.binding()) .toolbar { ToolbarItem(placement: .principal) { HStack { Button { } label: { Image(systemName: "rectangle.and.pencil.and.ellipsis") } Button { } label: { Image(systemName: "pawprint.circle") } } } } } } }
-
8:56 - Inspector with NavigationSplitView: detail column
NavigationSplitView { Sidebar() } detail: { AnimalTable() .inspector(presented: $isPresented) { AnimalInspectorForm() } }
-
9:06 - Inspector with NavigationSplitView: Outside
NavigationSplitView { Sidebar() } detail: { AnimalTable() } .inspector(presented: $isPresented) { AnimalInspectorForm() }
-
9:49 - Presentation customizations
.sheet(item: $nibbledFruit) { fruit in FruitNibbleBulletin(fruit: fruit) .presentationBackground(.thinMaterial) .presentationDetents([.height(200), .medium, .large]) .presentationBackgroundInteraction(.enabled(upThrough: .height(200))) }
-
11:58 - Presentation customizations on Inspector
.inspector(presented: $state.inspectorPresented) { AnimalInspectorForm(animal: $state.binding()) .presentationDetents([.height(200), .medium, .large]) .presentationBackgroundInteraction(.enabled(upThrough: .height(200))) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。