SwiftUI Sheet、导航与 Inspector 模式参考
目录
- Sheet 模式
- 导航模式
- 使用 NavigationSplitView 的多列导航
- Inspector
- 呈现修饰符
- 汇总清单
Sheet 模式
项目驱动的 Sheet(首选)
呈现基于模型的内容时,使用.sheet(item:)而不是.sheet(isPresented:)。
// 好 - 项目驱动@StateprivatevarselectedItem:Item?varbody:someView{List(items){iteminButton(item.name){selectedItem=item}}.sheet(item:$selectedItem){iteminItemDetailSheet(item:item)}}// 避免 - 布尔标志需要单独的状态@StateprivatevarshowSheet=false@StateprivatevarselectedItem:Item?varbody:someView{List(items){iteminButton(item.name){selectedItem=item showSheet=true}}.sheet(isPresented:$showSheet){ifletselectedItem{ItemDetailSheet(item:selectedItem)}}}原因:.sheet(item:)自动处理呈现状态,并避免在 sheet body 中进行可选解包。
Sheet 自己拥有其动作
Sheet 应该使用@Environment(\.dismiss)在内部处理自己的关闭和动作。避免从父视图传递onSave/onCancel闭包——这会创建回调逐层传递,并降低可复用性。
structEditItemSheet:View{@Environment(\.dismiss)privatevardismissletitem:Item@Stateprivatevarname:Stringinit(item:Item){self.item=item _name=State(initialValue:item.name)}varbody:someView{NavigationStack{Form{TextField("Name",text:$name)}.navigationTitle("Edit Item").toolbar{ToolbarItem(placement:.cancellationAction){Button("Cancel"){dismiss()}}ToolbarItem(placement:.confirmationAction){Button("Save"){/* 保存并关闭 */}}}}}}基于枚举的 Sheet 管理
当呈现多个不同的 sheet 时,使用遵循Identifiable的枚举搭配.sheet(item:),而不是多个布尔状态属性:
structArticlesView:View{enumSheet:Identifiable{caseadd,edit(Article),categoriesvarid:String{switchself{case.add:"add"case.edit(leta):"edit-\(a.id)"case.categories:"categories"}}}@StateprivatevarpresentedSheet:Sheet?varbody:someView{List{/* ... */}.toolbar{Button("Add"){presentedSheet=.add}}.sheet(item:$presentedSheet){sheetinswitchsheet{case.add:AddArticleView()case.edit(letarticle):EditArticleView(article:article)case.categories:CategoriesView()}}}}原因:单个@State属性和一个.sheet(item:)修饰符取代 N 个布尔属性和 N 个 sheet 修饰符,提高了可读性并防止同一时间只能有一个 sheet 的冲突。
导航模式
使用 NavigationStack 的类型安全导航
structContentView:View{varbody:someView{NavigationStack{List{NavigationLink("Profile",value:Route.profile)NavigationLink("Settings",value:Route.settings)}.navigationDestination(for:Route.self){routeinswitchroute{case.profile:ProfileView()case.settings:SettingsView()}}}}}enumRoute:Hashable{caseprofilecasesettings}编程式导航
structContentView:View{@StateprivatevarnavigationPath=NavigationPath()varbody:someView{NavigationStack(path:$navigationPath){List{Button("Go to Detail"){navigationPath.append(DetailRoute.item(id:1))}}.navigationDestination(for:DetailRoute.self){routeinswitchroute{case.item(letid):ItemDetailView(id:id)}}}}}enumDetailRoute:Hashable{caseitem(id:Int)}使用 NavigationSplitView 的多列导航
两列布局
对侧边栏驱动的导航使用NavigationSplitView。在 iOS 16+、macOS 13+、tvOS 16+、watchOS 9+ 上可用。
structContentView:View{@StateprivatevarselectedItem:Item.ID?varbody:someView{NavigationSplitView{List(items,selection:$selectedItem){iteminText(item.name)}.navigationTitle("Items")}detail:{ifletselectedItem,letitem=items.first(where:{$0.id==selectedItem}){ItemDetailView(item:item)}else{ContentUnavailableView("Select an Item",systemImage:"doc")}}}}三列布局
structContentView:View{@StateprivatevardepartmentId:Department.ID?@StateprivatevaremployeeIds=Set<Employee.ID>()varbody:someView{NavigationSplitView{List(model.departments,selection:$departmentId){deptinText(dept.name)}}content:{ifletdepartment=model.department(id:departmentId){List(department.employees,selection:$employeeIds){empinText(emp.name)}}else{Text("Select a department")}}detail:{EmployeeDetails(for:employeeIds)}}}配置
- 列可见性:
NavigationSplitView(columnVisibility: $visibility)搭配NavigationSplitViewVisibility(.detailOnly、.doubleColumn、.all) - 列宽:每列上使用
.navigationSplitViewColumnWidth(min:ideal:max:) - 紧凑列:
NavigationSplitView(preferredCompactColumn: $column)控制窄设备上显示哪一列 - 样式:
.navigationSplitViewStyle(.balanced)或.prominentDetail(默认)
平台行为
| 平台 | 行为 |
|---|---|
| macOS | 列始终并排可见;侧边栏有半透明材质;可通过拖动调整列宽 |
| iPadOS(常规) | 侧边栏可以覆盖或推开详情;支持通过工具栏按钮切换列可见性 |
| iOS / iPadOS(紧凑) | 折叠成单个NavigationStack;侧边栏项显示展开箭头;返回按钮在列之间导航 |
| iPhone(所有尺寸) | 始终折叠成堆栈;侧边栏作为根列表出现;选择将详情推入堆栈 |
| watchOS / tvOS | 折叠成单个堆栈 |
Inspector
可用性:iOS 17.0+、macOS 14.0+
用于补充信息的尾缘面板。
在较宽的尺寸类别(macOS、iPad 横屏)下,它显示为尾缘列。在紧凑尺寸类别(iPhone)下,它自动适配为 sheet。
基本 Inspector
structShapeEditor:View{@StateprivatevarshowInspector=falsevarbody:someView{MyEditorView().inspector(isPresented:$showInspector){InspectorContent()}.toolbar{ToolbarItem{Button{showInspector.toggle()}label:{Label("Inspector",systemImage:"info.circle")}}}}}带列宽的 Inspector
MyEditorView().inspector(isPresented:$showInspector){InspectorContent().inspectorColumnWidth(min:200,ideal:250,max:400)}带固定宽度的 Inspector
MyEditorView().inspector(isPresented:$showInspector){InspectorContent().inspectorColumnWidth(300)}平台行为
| 平台 | 行为 |
|---|---|
| macOS | 尾缘侧边栏面板;可通过拖动边缘调整大小;与窗口工具栏集成 |
| iPadOS(常规) | 与内容并排的尾缘列;可通过工具栏按钮切换 |
| iOS / iPadOS(紧凑) | 适配为 sheet 呈现;支持滑动关闭 |
| iPhone(所有尺寸) | 总是以 sheet 形式呈现(无尾缘列);通过滑动或按钮关闭 |
提示:在应用的
.commands中使用InspectorCommands以包含默认的 inspector 切换键盘快捷键。
呈现修饰符
全屏覆盖
structContentView:View{@StateprivatevarshowFullScreen=falsevarbody:someView{Button("Show Full Screen"){showFullScreen=true}.fullScreenCover(isPresented:$showFullScreen){FullScreenView()}}}Popover
structContentView:View{@StateprivatevarshowPopover=falsevarbody:someView{Button("Show Popover"){showPopover=true}.popover(isPresented:$showPopover){PopoverContentView().presentationCompactAdaptation(.popover)// 在 iPhone 上不适配为 sheet}}}关于alert和confirmationDialogAPI 模式,请参阅latest-apis.md。
汇总清单
- 基于模型的 sheet 使用
.sheet(item:) - Sheet 在内部拥有自己的动作和关闭
- 类型安全导航使用带
navigationDestination(for:)的NavigationStack - 编程式导航使用
NavigationPath - 侧边栏驱动的多列布局使用
NavigationSplitView - 尾缘补充面板使用
Inspector - 使用
navigationSplitViewColumnWidth(min:ideal:max:)或inspectorColumnWidth(min:ideal:max:)设置列宽 - 使用合适的呈现修饰符(sheet、fullScreenCover、popover)
- Alert 和确认对话框使用带 actions 的现代 API
- 避免向 sheet 传递 dismiss/save 回调
- 呈现多个 sheet 时,使用基于枚举的
Identifiable类型搭配.sheet(item:) - 需要时可保存/恢复导航状态