☰
swiftui-expert-skill - sheet-navigation-patterns
2026/10/12 3:42:32 网站建设 项目流程

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:)
  • 需要时可保存/恢复导航状态

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询