mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
Replace app scaffolding wiring with app wiring and dependencies
Removed the old 'app-scaffolding-wiring.md' reference and added a new, more comprehensive 'app-wiring.md' guide covering TabView, NavigationStack, sheet routing, and global dependency graph setup. Updated 'components-index.md' to reference the new guide and reflect the expanded focus on dependency management.
This commit is contained in:
parent
2f499fccee
commit
b6d69af073
3 changed files with 195 additions and 105 deletions
|
|
@ -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`
|
||||
194
swiftui-ui-patterns/references/app-wiring.md
Normal file
194
swiftui-ui-patterns/references/app-wiring.md
Normal file
|
|
@ -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<SheetDestination?>) -> 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue