diff --git a/swiftui-ui-patterns/references/components-index.md b/swiftui-ui-patterns/references/components-index.md index c5235a2..494ffbe 100644 --- a/swiftui-ui-patterns/references/components-index.md +++ b/swiftui-ui-patterns/references/components-index.md @@ -25,6 +25,7 @@ Use this file to find component-specific guidance. Each entry lists when to use - Matched transitions: `references/matched-transitions.md` — Use for smooth source-to-destination animations. - Deep links and URL routing: `references/deeplinks.md` — Use for in-app navigation from URLs. - Title menus: `references/title-menus.md` — Use for filter or context menus in the navigation title. +- Loading & placeholders: `references/loading-placeholders.md` — Use for redacted skeletons, empty states, and loading UX. ## Planned components (create files as needed) diff --git a/swiftui-ui-patterns/references/loading-placeholders.md b/swiftui-ui-patterns/references/loading-placeholders.md new file mode 100644 index 0000000..7014458 --- /dev/null +++ b/swiftui-ui-patterns/references/loading-placeholders.md @@ -0,0 +1,38 @@ +# Loading & Placeholders + +Use this when a view needs a consistent loading state (skeletons, redaction, empty state) without blocking interaction. + +## Patterns to prefer + +- **Redacted placeholders** for list/detail content to preserve layout while loading. +- **ContentUnavailableView** for empty or error states after loading completes. +- **ProgressView** only for short, global operations (use sparingly in content-heavy screens). + +## Recommended approach + +1. Keep the real layout, render placeholder data, then apply `.redacted(reason: .placeholder)`. +2. For lists, show a fixed number of placeholder rows (avoid infinite spinners). +3. Switch to `ContentUnavailableView` when load finishes but data is empty. + +## Pitfalls + +- Don’t animate layout shifts during redaction; keep frames stable. +- Avoid nesting multiple spinners; use one loading indicator per section. +- Keep placeholder count small (3–6) to reduce jank on low-end devices. + +## Minimal usage + +```swift +VStack { + if isLoading { + ForEach(0..<3, id: \.self) { _ in + RowView(model: .placeholder()) + } + .redacted(reason: .placeholder) + } else if items.isEmpty { + ContentUnavailableView("No items", systemImage: "tray") + } else { + ForEach(items) { item in RowView(model: item) } + } +} +```