CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

sheets.mdskills/swiftui-navigation/references/

Sheets

Contents

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 SheetDestination enum that describes every modal and is Identifiable.
  • 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 presentedSheet directly.

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 equivalent sheet(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() }
    }
  }
}

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 id when they are mutually exclusive (e.g., editor flows).
  • Keep sheet views lightweight and composed from smaller views; avoid large monoliths.

iOS 26 Presentation Sizing

Control sheet dimensions with presentationSizing(_:) (iOS 18+):

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .presentationSizing(.form)
}

PresentationSizing values:

  • .automatic -- platform default
  • .page -- roughly paper size, for informational content
  • .form -- slightly narrower than page, for form-style UI
  • .fitted -- sized by the content's ideal size

Modifier methods for fine-tuning:

  • .fitted(horizontal:vertical:) -- constrain fitting to specific axes
  • .sticky(horizontal:vertical:) -- grow but do not shrink in specified dimensions

Dismissal Confirmation (macOS 15+ / iOS 26+)

Show a confirmation dialog when the user tries to dismiss a sheet with unsaved changes:

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
        .dismissalConfirmationDialog(
            "Discard changes?",
            shouldPresent: hasUnsavedChanges
        ) {
            Button("Discard", role: .destructive) { discardChanges() }
        }
}
  • Cancel action is included automatically and prevents dismissal
  • All other action buttons allow dismissal to proceed
  • Use .keyboardShortcut(.defaultAction) to set the default button

Pitfalls

  • Avoid mixing sheet(isPresented:) and sheet(item:) for the same concern; prefer a single enum.
  • Do not store heavy state inside SheetDestination; pass lightweight identifiers or models.
  • If multiple sheets can appear from the same screen, give them distinct id values.
  • Use presentationSizing(.form) for form sheets instead of hard-coding frame dimensions.
  • Always pair dismissalConfirmationDialog with a shouldPresent condition; showing it when there are no changes is confusing.

skills

CHANGELOG.md

README.md

tile.json