Add fourteen skills from Dimillian/Skills, integrate the merged Snowflake and WordPress updates into the maintainer sync, and refresh registry metadata, attributions, walkthrough notes, and the 8.9.0 release notes while keeping validation warnings within budget.
4.2 KiB
4.2 KiB
Sheets
Intent
Use a centralized sheet routing pattern so any view can present modals without prop-drilling. This keeps sheet state in one place and scales as the app grows.
Core architecture
- Define a
SheetDestinationenum that describes every modal and isIdentifiable. - Store the current sheet in a router object (
presentedSheet: SheetDestination?). - Create a view modifier like
withSheetDestinations(...)that maps the enum to concrete sheet views. - Inject the router into the environment so child views can set
presentedSheetdirectly.
Example: item-driven local sheet
Use this when sheet state is local to one screen and does not need centralized routing.
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
Example: SheetDestination enum
enum SheetDestination: Identifiable, Hashable {
case composer
case editProfile
case settings
case report(itemID: String)
var id: String {
switch self {
case .composer, .editProfile:
// Use the same id to ensure only one editor-like sheet is active at a time.
return "editor"
case .settings:
return "settings"
case .report:
return "report"
}
}
}
Example: withSheetDestinations modifier
extension View {
func withSheetDestinations(
sheet: Binding<SheetDestination?>
) -> some View {
sheet(item: sheet) { destination in
Group {
switch destination {
case .composer:
ComposerView()
case .editProfile:
EditProfileView()
case .settings:
SettingsView()
case .report(let itemID):
ReportView(itemID: itemID)
}
}
}
}
}
Example: presenting from a child view
struct StatusRow: View {
@Environment(RouterPath.self) private var router
var body: some View {
Button("Report") {
router.presentedSheet = .report(itemID: "123")
}
}
}
Required wiring
For the child view to work, a parent view must:
- own the router instance,
- attach
withSheetDestinations(sheet: $router.presentedSheet)(or an equivalentsheet(item:)handler), and - inject it with
.environment(router)after the sheet modifier so the modal content inherits it.
This makes the child assignment to router.presentedSheet drive presentation at the root.
Example: sheets that need their own navigation
Wrap sheet content in a NavigationStack so it can push within the modal.
struct NavigationSheet<Content: View>: View {
var content: () -> Content
var body: some View {
NavigationStack {
content()
.toolbar { CloseToolbarItem() }
}
}
}
Example: sheet owns its actions
Keep dismissal and confirmation logic inside the sheet when the actions belong to the modal itself.
struct EditItemSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(Store.self) private var store
let item: Item
@State private var isSaving = false
var body: some View {
VStack {
Button(isSaving ? "Saving..." : "Save") {
Task { await save() }
}
}
}
private func save() async {
isSaving = true
await store.save(item)
dismiss()
}
}
Design choices to keep
- Centralize sheet routing so features can present modals without wiring bindings through many layers.
- Use
sheet(item:)to guarantee a single sheet is active and to drive presentation from the enum. - Group related sheets under the same
idwhen they are mutually exclusive (e.g., editor flows). - Keep sheet views lightweight and composed from smaller views; avoid large monoliths.
- Let sheets own their actions and call
dismiss()internally instead of forwardingonCanceloronConfirmclosures through many layers.
Pitfalls
- Avoid mixing
sheet(isPresented:)andsheet(item:)for the same concern; prefer a single enum. - Avoid
if letinside a sheet body when the presentation state already carries the selected model; prefersheet(item:). - Do not store heavy state inside
SheetDestination; pass lightweight identifiers or models. - If multiple sheets can appear from the same screen, give them distinct
idvalues.