diff --git a/swiftui-ui-patterns/references/app-scaffolding-wiring.md b/swiftui-ui-patterns/references/app-scaffolding-wiring.md deleted file mode 100644 index a569a72..0000000 --- a/swiftui-ui-patterns/references/app-scaffolding-wiring.md +++ /dev/null @@ -1,104 +0,0 @@ -# App scaffolding wiring - -## Intent - -Show how `TabView`, `NavigationStack`, and sheet routing fit together at the app root and per-tab level. - -## Recommended wiring (root + per-tab) - -```swift -@MainActor -struct AppView: View { - @State private var selectedTab: AppTab = .timeline - @State private var tabRouter = TabRouter() - - var body: some View { - TabView(selection: $selectedTab) { - ForEach(AppTab.allCases) { tab in - let router = tabRouter.router(for: tab) - NavigationStack(path: tabRouter.binding(for: tab)) { - tab.makeContentView() - } - .withSheetDestinations(sheet: Binding( - get: { router.presentedSheet }, - set: { router.presentedSheet = $0 } - )) - .environment(router) - .tabItem { tab.label } - .tag(tab) - } - } - } -} -``` - -## Minimal AppTab skeleton - -```swift -@MainActor -enum AppTab: Identifiable, Hashable, CaseIterable { - case timeline - case notifications - case settings - - var id: String { - switch self { - case .timeline: return "timeline" - case .notifications: return "notifications" - case .settings: return "settings" - } - } - - @ViewBuilder - func makeContentView() -> some View { - switch self { - case .timeline: - TimelineView() - case .notifications: - NotificationsView() - case .settings: - SettingsView() - } - } - - @ViewBuilder - var label: some View { - switch self { - case .timeline: - Label("Timeline", systemImage: "rectangle.stack") - case .notifications: - Label("Notifications", systemImage: "bell") - case .settings: - Label("Settings", systemImage: "gear") - } - } -} -``` - -## Minimal RouterPath skeleton - -```swift -@MainActor -@Observable -final class RouterPath { - var path: [Route] = [] - var presentedSheet: SheetDestination? -} - -enum Route: Hashable { - case account(id: String) - case status(id: String) -} -``` - -## Notes - -- Each tab owns an independent navigation history via its own router. -- Sheets are routed from any child view by setting `router.presentedSheet`. -- Use the `TabRouter` pattern when tabs are data-driven; use one router per tab if tabs are fixed. - -## Related references - -- TabView: `references/tabview.md` -- NavigationStack: `references/navigationstack.md` -- Sheets: `references/sheets.md` diff --git a/swiftui-ui-patterns/references/app-wiring.md b/swiftui-ui-patterns/references/app-wiring.md new file mode 100644 index 0000000..a6734e1 --- /dev/null +++ b/swiftui-ui-patterns/references/app-wiring.md @@ -0,0 +1,194 @@ +# App wiring and dependency graph + +## Intent + +Show how to wire the app shell (TabView + NavigationStack + sheets) and install a global dependency graph (environment objects, services, streaming clients, SwiftData ModelContainer) in one place. + +## Recommended structure + +1) Root view sets up tabs, per-tab routers, and sheets. +2) A dedicated view modifier installs global dependencies and lifecycle tasks (auth state, streaming watchers, push tokens, data containers). +3) Feature views pull only what they need from the environment; feature-specific state stays local. + +## Root shell example (generic) + +```swift +@MainActor +struct AppView: View { + @State private var selectedTab: AppTab = .home + @State private var tabRouter = TabRouter() + + var body: some View { + TabView(selection: $selectedTab) { + ForEach(AppTab.allCases) { tab in + let router = tabRouter.router(for: tab) + NavigationStack(path: tabRouter.binding(for: tab)) { + tab.makeContentView() + } + .withSheetDestinations(sheet: Binding( + get: { router.presentedSheet }, + set: { router.presentedSheet = $0 } + )) + .environment(router) + .tabItem { tab.label } + .tag(tab) + } + } + .withAppDependencyGraph() + } +} +``` + +Minimal `AppTab` example: + +```swift +@MainActor +enum AppTab: Identifiable, Hashable, CaseIterable { + case home, notifications, settings + var id: String { String(describing: self) } + + @ViewBuilder + func makeContentView() -> some View { + switch self { + case .home: HomeView() + case .notifications: NotificationsView() + case .settings: SettingsView() + } + } + + @ViewBuilder + var label: some View { + switch self { + case .home: Label("Home", systemImage: "house") + case .notifications: Label("Notifications", systemImage: "bell") + case .settings: Label("Settings", systemImage: "gear") + } + } +} +``` + +Router skeleton: + +```swift +@MainActor +@Observable +final class RouterPath { + var path: [Route] = [] + var presentedSheet: SheetDestination? +} + +enum Route: Hashable { + case detail(id: String) +} +``` + +## Dependency graph modifier (generic) + +Use a single modifier to install environment objects and handle lifecycle hooks when the active account/client changes. This keeps wiring consistent and avoids forgetting a dependency in call sites. + +```swift +extension View { + func withAppDependencyGraph( + accountManager: AccountManager = .shared, + currentAccount: CurrentAccount = .shared, + currentInstance: CurrentInstance = .shared, + userPreferences: UserPreferences = .shared, + theme: Theme = .shared, + watcher: StreamWatcher = .shared, + pushNotifications: PushNotificationsService = .shared, + intentService: AppIntentService = .shared, + quickLook: QuickLook = .shared, + toastCenter: ToastCenter = .shared, + namespace: Namespace.ID? = nil, + isSupporter: Bool = false + ) -> some View { + environment(accountManager) + .environment(accountManager.currentClient) + .environment(quickLook) + .environment(currentAccount) + .environment(currentInstance) + .environment(userPreferences) + .environment(theme) + .environment(watcher) + .environment(pushNotifications) + .environment(intentService) + .environment(toastCenter) + .environment(\.isSupporter, isSupporter) + .task(id: accountManager.currentClient.id) { + let client = accountManager.currentClient + if let namespace { quickLook.namespace = namespace } + currentAccount.setClient(client: client) + currentInstance.setClient(client: client) + userPreferences.setClient(client: client) + await currentInstance.fetchCurrentInstance() + watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.streamingURL) + if client.isAuth { + watcher.watch(streams: [.user, .direct]) + } else { + watcher.stopWatching() + } + } + .task(id: accountManager.pushAccounts.map(\.token)) { + pushNotifications.tokens = accountManager.pushAccounts.map(\.token) + } + } +} +``` + +Notes: +- The `.task(id:)` hooks respond to account/client changes, re-seeding services and watcher state. +- Keep the modifier focused on global wiring; feature-specific state stays within features. +- Adjust types (AccountManager, StreamWatcher, etc.) to match your project. + +## SwiftData / ModelContainer + +Install your `ModelContainer` at the root so all feature views share the same store. Keep the list minimal to the models that need persistence. + +```swift +extension View { + func withModelContainer() -> some View { + modelContainer(for: [Draft.self, LocalTimeline.self, TagGroup.self]) + } +} +``` + +Why: a single container avoids duplicated stores per sheet or tab and keeps data consistent. + +## Sheet routing (enum-driven) + +Centralize sheets with a small enum and a helper modifier. + +```swift +enum SheetDestination: Identifiable { + case composer + case settings + var id: String { String(describing: self) } +} + +extension View { + func withSheetDestinations(sheet: Binding) -> some View { + sheet(item: sheet) { destination in + switch destination { + case .composer: + ComposerView().withEnvironments() + case .settings: + SettingsView().withEnvironments() + } + } + } +} +``` + +Why: enum-driven sheets keep presentation centralized and testable; adding a new sheet means adding one enum case and one switch branch. + +## When to use + +- Apps with multiple packages/modules that share environment objects and services. +- Apps that need to react to account/client changes and rewire streaming/push safely. +- Any app that wants consistent TabView + NavigationStack + sheet wiring without repeating environment setup. + +## Caveats + +- Keep the dependency modifier slim; do not put feature state or heavy logic there. +- Ensure `.task(id:)` work is lightweight or cancelled appropriately; long-running work belongs in services. +- If unauthenticated clients exist, gate streaming/watch calls to avoid reconnect spam. diff --git a/swiftui-ui-patterns/references/components-index.md b/swiftui-ui-patterns/references/components-index.md index 494ffbe..d6c4a89 100644 --- a/swiftui-ui-patterns/references/components-index.md +++ b/swiftui-ui-patterns/references/components-index.md @@ -7,7 +7,7 @@ Use this file to find component-specific guidance. Each entry lists when to use - TabView: `references/tabview.md` — Use when building a tab-based app or any tabbed feature set. - NavigationStack: `references/navigationstack.md` — Use when you need push navigation and programmatic routing, especially per-tab history. - Sheets and modal routing: `references/sheets.md` — Use when you want centralized, enum-driven sheet presentation. -- App scaffolding wiring: `references/app-scaffolding-wiring.md` — Use to wire TabView + NavigationStack + sheets at the root. +- App wiring and dependency graph: `references/app-wiring.md` — Use to wire TabView + NavigationStack + sheets at the root and install global dependencies. - Form and Settings: `references/form.md` — Use for settings, grouped inputs, and structured data entry. - Split views and columns: `references/split-views.md` — Use for iPad/macOS multi-column layouts or custom secondary columns. - List and Section: `references/list.md` — Use for feed-style content and settings rows.