Add Claude Code support for three Swift/SwiftUI skills

Migrate swiftui-ui-patterns, swift-concurrency-expert, and swiftui-liquid-glass
skills to the .claude/skills directory structure for Claude Code compatibility.

The skills use the same SKILL.md format (standardized across both platforms),
so no changes to the skill content were needed. Updated README with installation
instructions for both Claude Code and OpenAI Codex.
This commit is contained in:
Claude 2026-01-10 13:28:51 +00:00
parent 9826f404c3
commit 0950da5380
No known key found for this signature in database
34 changed files with 3036 additions and 2 deletions

View file

@ -0,0 +1,37 @@
---
name: swift-concurrency-expert
description: Swift Concurrency review and remediation for Swift 6.2+. Use when asked to review Swift Concurrency usage, improve concurrency compliance, or fix Swift concurrency compiler errors in a feature or file.
---
# Swift Concurrency Expert
## Overview
Review and fix Swift Concurrency issues in Swift 6.2+ codebases by applying actor isolation, Sendable safety, and modern concurrency patterns with minimal behavior changes.
## Workflow
### 1. Triage the issue
- Capture the exact compiler diagnostics and the offending symbol(s).
- Check project concurrency settings: Swift language version (6.2+), strict concurrency level, and whether approachable concurrency (default actor isolation / main-actor-by-default) is enabled.
- Identify the current actor context (`@MainActor`, `actor`, `nonisolated`) and whether a default actor isolation mode is enabled.
- Confirm whether the code is UI-bound or intended to run off the main actor.
### 2. Apply the smallest safe fix
Prefer edits that preserve existing behavior while satisfying data-race safety.
Common fixes:
- **UI-bound types**: annotate the type or relevant members with `@MainActor`.
- **Protocol conformance on main actor types**: make the conformance isolated (e.g., `extension Foo: @MainActor SomeProtocol`).
- **Global/static state**: protect with `@MainActor` or move into an actor.
- **Background work**: move expensive work into a `@concurrent` async function on a `nonisolated` type or use an `actor` to guard mutable state.
- **Sendable errors**: prefer immutable/value types; add `Sendable` conformance only when correct; avoid `@unchecked Sendable` unless you can prove thread safety.
## Reference material
- See `references/swift-6-2-concurrency.md` for Swift 6.2 changes, patterns, and examples.
- See `references/approachable-concurrency.md` when the project is opted into approachable concurrency mode.
- See `references/swiftui-concurrency-tour-wwdc.md` for SwiftUI-specific concurrency guidance.

View file

@ -0,0 +1,63 @@
## Approachable Concurrency (Swift 6.2) - project mode quick guide
Use this reference when the project has opted into the Swift 6.2 approachable
concurrency settings (default actor isolation / main-actor-by-default).
## Detect the mode
Check Xcode build settings under "Swift Compiler - Concurrency":
- Swift language version (must be 6.2+).
- Default actor isolation / Main Actor by default.
- Strict concurrency checking level (Complete/Targeted/Minimal).
For SwiftPM, inspect Package.swift swiftSettings for the same flags.
## Behavior changes to expect
- Async functions stay on the caller's actor by default; they don't hop to a
global concurrent executor unless the implementation chooses to.
- Main-actor-by-default reduces data race errors for UI-bound code and global
state, because mutable state is implicitly protected.
- Protocol conformances can be isolated (e.g., `extension Foo: @MainActor Bar`).
## How to apply fixes in this mode
- Prefer minimal annotations; let main-actor-by-default do the work when the
code is UI-bound.
- Use isolated conformances instead of forcing `nonisolated` workarounds.
- Keep global or shared mutable state on the main actor unless there is a clear
performance need to offload it.
## When to opt out or offload work
- Use `@concurrent` on async functions that must run on the concurrent pool.
- Make types or members `nonisolated` only when they are truly thread-safe and
used off the main actor.
- Continue to respect Sendable boundaries when values cross actors or tasks.
## Common pitfalls
- `Task.detached` ignores inherited actor context; avoid it unless you truly
need to break isolation.
- Main-actor-by-default can hide performance issues if CPU-heavy work stays on
the main actor; move that work into `@concurrent` async functions.
## Keywords (from source cheat sheet)
| Keyword | What it does |
| --- | --- |
| `async` | Function can pause |
| `await` | Pause here until done |
| `Task { }` | Start async work, inherits context |
| `Task.detached { }` | Start async work, no inherited context |
| `@MainActor` | Runs on main thread |
| `actor` | Type with isolated mutable state |
| `nonisolated` | Opts out of actor isolation |
| `Sendable` | Safe to pass between isolation domains |
| `@concurrent` | Always run on background (Swift 6.2+) |
| `async let` | Start parallel work |
| `TaskGroup` | Dynamic parallel work |
## Source
https://fuckingapproachableswiftconcurrency.com/en/

View file

@ -0,0 +1,272 @@
## Concurrent programming updates in Swift 6.2
Concurrent programming is hard because sharing memory between multiple tasks is prone to mistakes that lead to unpredictable behavior.
## Data-race safety
Data-race safety in Swift 6 prevents these mistakes at compile time, so you can write concurrent code without fear of introducing hard-to-debug runtime bugs. But in many cases, the most natural code to write is prone to data races, leading to compiler errors that you have to address. A class with mutable state, like this `PhotoProcessor` class, is safe as long as you dont access it concurrently.
```swift
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? { }
}
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}
// Error: Sending 'self.photoProcessor' risks causing data races
// Sending main actor-isolated 'self.photoProcessor' to nonisolated instance method 'extractSticker(data:with:)'
// risks causing data races between nonisolated and main actor-isolated uses
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
```
It has an async method to extract a `Sticker` by computing the subject of the given image data. But if you try to call `extractSticker` from UI code on the main actor, youll get an error that the call risks causing data races. This is because there are several places in the language that offload work to the background implicitly, even if you never needed code to run in parallel.
Swift 6.2 changes this philosophy to stay single threaded by default until you choose to introduce concurrency.
```swift
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? { }
}
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}
// No longer a data race error in Swift 6.2 because of Approachable Concurrency and default actor isolation
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
```
The language changes in Swift 6.2 make the most natural code to write data race free by default. This provides a more approachable path to introducing concurrency in a project.
When you choose to introduce concurrency because you want to run code in parallel, data-race safety will protect you.
First, we've made it easier to call async functions on types with mutable state. Instead of eagerly offloading async functions that aren't tied to a specific actor, the function will continue to run on the actor it was called from. This eliminates data races because the values passed into the async function are never sent outside the actor. Async functions can still offload work in their implementation, but clients dont have to worry about their mutable state.
Next, weve made it easier to implement conformances on main actor types. Here I have a protocol called `Exportable`, and Im trying to implement a conformance for my main actor `StickerModel` class. The export requirement doesnt have actor isolation, so the language assumed that it could be called from off the main actor, and prevented `StickerModel` from using main actor state in its implementation.
```swift
protocol Exportable {
func export()
}
extension StickerModel: Exportable { // error: Conformance of 'StickerModel' to protocol 'Exportable' crosses into main actor-isolated code and can cause data races
func export() {
photoProcessor.exportAsPNG()
}
}
```
Swift 6.2 supports these conformances. A conformance that needs main actor state is called an *isolated* conformance. This is safe because the compiler ensures a main actor conformance is only used on the main actor.
```swift
// Isolated conformances
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
```
I can create an `ImageExporter` type that adds a `StickerModel` to an array of any `Exportable` items as long as it stays on the main actor.
```swift
// Isolated conformances
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item)
}
func exportAll() {
for item in items {
item.export()
}
}
}
```
But if I allow `ImageExporter` to be used from anywhere, the compiler prevents adding `StickerModel` to the array because it isnt safe to call export on `StickerModel` from outside the main actor.
```swift
// Isolated conformances
nonisolated
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // error: Main actor-isolated conformance of 'StickerModel' to 'Exportable' cannot be used in nonisolated context
}
func exportAll() {
for item in items {
item.export()
}
}
}
```
With isolated conformances, you only have to solve data race safety issues when the code indicates that it uses the conformance concurrently.
## Global State
Global and static variables are prone to data races because they allow mutable state to be accessed from anywhere.
```swift
final class StickerLibrary {
static let shared: StickerLibrary = .init() // error: Static property 'shared' is not concurrency-safe because non-'Sendable' type 'StickerLibrary' may have shared mutable state
}
```
The most common way to protect global state is with the main actor.
```swift
final class StickerLibrary {
@MainActor
static let shared: StickerLibrary = .init()
}
```
And its common to annotate an entire class with the main actor to protect all of its mutable state, especially in a project that doesnt have a lot of concurrent tasks.
```swift
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
```
You can model a program that's entirely single-threaded by writing `@MainActor` on everything in your project.
```swift
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
@MainActor
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem]
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
```
To make it easier to model single-threaded code, weve introduced a mode to infer main actor by default.
```swift
// Mode to infer main actor by default in Swift 6.2
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem]
}
extension StickerModel: Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
```
This eliminates data-race safety errors about unsafe global and static variables, calls to other main actor functions like ones from the SDK, and more, because the main actor protects all mutable state by default. It also reduces concurrency annotations in code thats mostly single-threaded. This mode is great for projects that do most of the work on the main actor, and concurrent code is encapsulated within specific types or files. Its opt-in and its recommended for apps, scripts, and other executable targets.
## Offloading work to the background
Offloading work to the background is still important for performance, such as keeping apps responsive when performing CPU-intensive tasks.
Lets look at the implementation of the `extractSticker` method on `PhotoProcessor`.
```swift
// Explicitly offloading async work
class PhotoProcessor {
var cachedStickers: [String: Sticker]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] {
return sticker
}
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
// Offload expensive image processing using the @concurrent attribute.
@concurrent
static func extractSubject(from data: Data) async -> Sticker { }
}
```
It first checks whether it already extracted a sticker for an image, so it can return the cached sticker immediately. If the sticker hasnt been cached, it extracts the subject from the image data and creates a new sticker. The `extractSubject` method performs expensive image processing that I dont want to block the main actor or any other actor.
I can offload this work using the `@concurrent` attribute. `@concurrent` ensures that a function always runs on the concurrent thread pool, freeing up the actor to run other tasks at the same time.
### An example
Say you have a function called `process` that you would like to run on a background thread. To call that function on a background thread you need to:
- make sure the structure or class is `nonisolated`
- add the `@concurrent` attribute to the function you want to run in the background
- add the keyword `async` to the function if it is not already asynchronous
- and then add the keyword `await` to any callers
Like this:
```swift
nonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { ... }
}
// Callers with the added await
processedPhotos[item.id] = await PhotoProcessor().process(data: data)
```
## Summary
These language changes work together to make concurrency more approachable.
You start by writing code that runs on the main actor by default, where theres no risk of data races. When you start to use async functions, those functions run wherever theyre called from. Theres still no risk of data races because all of your code still runs on the main actor. When youre ready to embrace concurrency to improve performance, its easy to offload specific code to the background to run in parallel.
Some of these language changes are opt-in because they require changes in your project to adopt. You can find and enable all of the approachable concurrency language changes under the Swift Compiler - Concurrency section of Xcode build settings. You can also enable these features in a Swift package manifest file using the SwiftSettings API.
Swift 6.2 includes migration tooling to help you make the necessary code changes automatically. You can learn more about migration tooling at swift.org/migration.

View file

@ -0,0 +1,33 @@
# SwiftUI Concurrency Tour (Summary)
Context: SwiftUI-focused concurrency overview covering actor isolation, Sendable closures, and how SwiftUI runs work off the main thread.
## Main-actor default in SwiftUI
- `View` is `@MainActor` isolated by default; members and `body` inherit isolation.
- Swift 6.2 can infer `@MainActor` for all types in a module (new language mode).
- This default simplifies UI code and aligns with UIKit/AppKit `@MainActor` APIs.
## Where SwiftUI runs code off the main thread
- SwiftUI may evaluate some view logic on background threads for performance.
- Examples: `Shape` path generation, `Layout` methods, `visualEffect` closures, and `onGeometryChange` closures.
- These APIs often require `Sendable` closures to reflect their runtime semantics.
## Sendable closures and data-race safety
- Accessing `@MainActor` state from a `Sendable` closure is unsafe and flagged by the compiler.
- Prefer capturing value copies in the closure capture list (e.g., copy a `Bool`).
- Avoid sending `self` into a sendable closure just to read a single property.
## Structuring async work with SwiftUI
- SwiftUI action callbacks are synchronous so UI updates (like loading states) can be immediate.
- Use `Task` to bridge into async contexts; keep async bodies minimal.
- Use state as the boundary: async work updates model/state; UI reacts synchronously.
## Performance-driven concurrency
- Offload expensive work from the main actor to avoid hitches.
- Keep time-sensitive UI logic (animations, gesture responses) synchronous.
- Separate UI code from long-running async work to improve responsiveness and testability.

View file

@ -0,0 +1,90 @@
---
name: swiftui-liquid-glass
description: Implement, review, or improve SwiftUI features using the iOS 26+ Liquid Glass API. Use when asked to adopt Liquid Glass in new SwiftUI UI, refactor an existing feature to Liquid Glass, or review Liquid Glass usage for correctness, performance, and design alignment.
---
# SwiftUI Liquid Glass
## Overview
Use this skill to build or review SwiftUI features that fully align with the iOS 26+ Liquid Glass API. Prioritize native APIs (`glassEffect`, `GlassEffectContainer`, glass button styles) and Apple design guidance. Keep usage consistent, interactive where needed, and performance aware.
## Workflow Decision Tree
Choose the path that matches the request:
### 1) Review an existing feature
- Inspect where Liquid Glass should be used and where it should not.
- Verify correct modifier order, shape usage, and container placement.
- Check for iOS 26+ availability handling and sensible fallbacks.
### 2) Improve a feature using Liquid Glass
- Identify target components for glass treatment (surfaces, chips, buttons, cards).
- Refactor to use `GlassEffectContainer` where multiple glass elements appear.
- Introduce interactive glass only for tappable or focusable elements.
### 3) Implement a new feature using Liquid Glass
- Design the glass surfaces and interactions first (shape, prominence, grouping).
- Add glass modifiers after layout/appearance modifiers.
- Add morphing transitions only when the view hierarchy changes with animation.
## Core Guidelines
- Prefer native Liquid Glass APIs over custom blurs.
- Use `GlassEffectContainer` when multiple glass elements coexist.
- Apply `.glassEffect(...)` after layout and visual modifiers.
- Use `.interactive()` for elements that respond to touch/pointer.
- Keep shapes consistent across related elements for a cohesive look.
- Gate with `#available(iOS 26, *)` and provide a non-glass fallback.
## Review Checklist
- **Availability**: `#available(iOS 26, *)` present with fallback UI.
- **Composition**: Multiple glass views wrapped in `GlassEffectContainer`.
- **Modifier order**: `glassEffect` applied after layout/appearance modifiers.
- **Interactivity**: `interactive()` only where user interaction exists.
- **Transitions**: `glassEffectID` used with `@Namespace` for morphing.
- **Consistency**: Shapes, tinting, and spacing align across the feature.
## Implementation Checklist
- Define target elements and desired glass prominence.
- Wrap grouped glass elements in `GlassEffectContainer` and tune spacing.
- Use `.glassEffect(.regular.tint(...).interactive(), in: .rect(cornerRadius: ...))` as needed.
- Use `.buttonStyle(.glass)` / `.buttonStyle(.glassProminent)` for actions.
- Add morphing transitions with `glassEffectID` when hierarchy changes.
- Provide fallback materials and visuals for earlier iOS versions.
## Quick Snippets
Use these patterns directly and tailor shapes/tints/spacing.
```swift
if #available(iOS 26, *) {
Text("Hello")
.padding()
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 16))
} else {
Text("Hello")
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
```
```swift
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
Image(systemName: "scribble.variable")
.frame(width: 72, height: 72)
.font(.system(size: 32))
.glassEffect()
Image(systemName: "eraser.fill")
.frame(width: 72, height: 72)
.font(.system(size: 32))
.glassEffect()
}
}
```
```swift
Button("Confirm") { }
.buttonStyle(.glassProminent)
```
## Resources
- Reference guide: `references/liquid-glass.md`
- Prefer Apple docs for up-to-date API details.

View file

@ -0,0 +1,280 @@
# Implementing Liquid Glass Design in SwiftUI
## Overview
Liquid Glass is a dynamic material introduced in iOS that combines the optical properties of glass with a sense of fluidity. It blurs content behind it, reflects color and light from surrounding content, and reacts to touch and pointer interactions in real time. This guide covers how to implement and customize Liquid Glass effects in SwiftUI applications. You should always seek guides on Liquid Glass when asked for help adopting new Apple design.
Key features of Liquid Glass:
- Blurs content behind the material
- Reflects color and light from surrounding content
- Reacts to touch and pointer interactions
- Can morph between shapes during transitions
- Available for standard and custom components
## Basic Implementation
### Adding Liquid Glass to a View
The simplest way to add Liquid Glass to a view is using the `glassEffect()` modifier:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect()
```
By default, this applies the regular variant of Glass within a Capsule shape behind the view's content.
### Customizing the Shape
You can specify a different shape for the Liquid Glass effect:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(in: .rect(cornerRadius: 16.0))
```
Common shape options:
- `.capsule` (default)
- `.rect(cornerRadius: CGFloat)`
- `.circle`
## Customizing Liquid Glass Effects
### Glass Variants and Properties
You can customize the Liquid Glass effect by configuring the `Glass` structure:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(.regular.tint(.orange).interactive())
```
Key customization options:
- `.regular` - Standard glass effect
- `.tint(Color)` - Add a color tint to suggest prominence
- `.interactive(Bool)` - Make the glass react to touch and pointer interactions
### Making Interactive Glass
To make Liquid Glass react to touch and pointer interactions:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(.regular.interactive(true))
```
Or more concisely:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(.regular.interactive())
```
## Working with Multiple Glass Effects
### Using GlassEffectContainer
When applying Liquid Glass effects to multiple views, use `GlassEffectContainer` for better rendering performance and to enable blending and morphing effects:
```swift
GlassEffectContainer(spacing: 40.0) {
HStack(spacing: 40.0) {
Image(systemName: "scribble.variable")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
Image(systemName: "eraser.fill")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
}
}
```
The `spacing` parameter controls how the Liquid Glass effects interact with each other:
- Smaller spacing: Views need to be closer to merge effects
- Larger spacing: Effects merge at greater distances
### Uniting Multiple Glass Effects
To combine multiple views into a single Liquid Glass effect, use the `glassEffectUnion` modifier:
```swift
@Namespace private var namespace
// Later in your view:
GlassEffectContainer(spacing: 20.0) {
HStack(spacing: 20.0) {
ForEach(symbolSet.indices, id: \.self) { item in
Image(systemName: symbolSet[item])
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
.glassEffectUnion(id: item < 2 ? "1" : "2", namespace: namespace)
}
}
}
```
This is useful when creating views dynamically or with views that live outside of an HStack or VStack.
## Morphing Effects and Transitions
### Creating Morphing Transitions
To create morphing effects during transitions between views with Liquid Glass:
1. Create a namespace using the `@Namespace` property wrapper
2. Associate each Liquid Glass effect with a unique identifier using `glassEffectID`
3. Use animations when changing the view hierarchy
```swift
@State private var isExpanded: Bool = false
@Namespace private var namespace
var body: some View {
GlassEffectContainer(spacing: 40.0) {
HStack(spacing: 40.0) {
Image(systemName: "scribble.variable")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
.glassEffectID("pencil", in: namespace)
if isExpanded {
Image(systemName: "eraser.fill")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
.glassEffectID("eraser", in: namespace)
}
}
}
Button("Toggle") {
withAnimation {
isExpanded.toggle()
}
}
.buttonStyle(.glass)
}
```
The morphing effect occurs when views with Liquid Glass appear or disappear due to view hierarchy changes.
## Button Styling with Liquid Glass
### Glass Button Style
SwiftUI provides built-in button styles for Liquid Glass:
```swift
Button("Click Me") {
// Action
}
.buttonStyle(.glass)
```
### Glass Prominent Button Style
For a more prominent glass button:
```swift
Button("Important Action") {
// Action
}
.buttonStyle(.glassProminent)
```
## Advanced Techniques
### Background Extension Effect
To stretch content behind a sidebar or inspector with the background extension effect:
```swift
NavigationSplitView {
// Sidebar content
} detail: {
// Detail content
.background {
// Background content that extends under the sidebar
}
}
```
### Extending Horizontal Scrolling Under Sidebar
To extend horizontal scroll views under a sidebar or inspector:
```swift
ScrollView(.horizontal) {
// Scrollable content
}
.scrollExtensionMode(.underSidebar)
```
## Best Practices
1. **Container Usage**: Always use `GlassEffectContainer` when applying Liquid Glass to multiple views for better performance and morphing effects.
2. **Effect Order**: Apply the `.glassEffect()` modifier after other modifiers that affect the appearance of the view.
3. **Spacing Consideration**: Carefully choose spacing values in containers to control how and when glass effects merge.
4. **Animation**: Use animations when changing view hierarchies to enable smooth morphing transitions.
5. **Interactivity**: Add `.interactive()` to glass effects that should respond to user interaction.
6. **Consistent Design**: Maintain consistent shapes and styles across your app for a cohesive look and feel.
## Example: Custom Badge with Liquid Glass
```swift
struct BadgeView: View {
let symbol: String
let color: Color
var body: some View {
ZStack {
Image(systemName: "hexagon.fill")
.foregroundColor(color)
.font(.system(size: 50))
Image(systemName: symbol)
.foregroundColor(.white)
.font(.system(size: 30))
}
.glassEffect(.regular, in: .rect(cornerRadius: 16))
}
}
// Usage:
GlassEffectContainer(spacing: 20) {
HStack(spacing: 20) {
BadgeView(symbol: "star.fill", color: .blue)
BadgeView(symbol: "heart.fill", color: .red)
BadgeView(symbol: "leaf.fill", color: .green)
}
}
```
## References
- [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/SwiftUI/Applying-Liquid-Glass-to-custom-views)
- [Landmarks: Building an app with Liquid Glass](https://developer.apple.com/documentation/SwiftUI/Landmarks-Building-an-app-with-Liquid-Glass)
- [SwiftUI View.glassEffect(_:in:isEnabled:)](https://developer.apple.com/documentation/SwiftUI/View/glassEffect(_:in:isEnabled:))
- [SwiftUI GlassEffectContainer](https://developer.apple.com/documentation/SwiftUI/GlassEffectContainer)
- [SwiftUI GlassEffectTransition](https://developer.apple.com/documentation/SwiftUI/GlassEffectTransition)
- [SwiftUI GlassButtonStyle](https://developer.apple.com/documentation/SwiftUI/GlassButtonStyle)

View file

@ -0,0 +1,95 @@
---
name: swiftui-ui-patterns
description: Best practices and example-driven guidance for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture with TabView, composing screens, or needing component-specific patterns and examples.
---
# SwiftUI UI Patterns
## Quick start
Choose a track based on your goal:
### Existing project
- Identify the feature or screen and the primary interaction model (list, detail, editor, settings, tabbed).
- Find a nearby example in the repo with `rg "TabView\("` or similar, then read the closest SwiftUI view.
- Apply local conventions: prefer SwiftUI-native state, keep state local when possible, and use environment injection for shared dependencies.
- Choose the relevant component reference from `references/components-index.md` and follow its guidance.
- Build the view with small, focused subviews and SwiftUI-native data flow.
### New project scaffolding
- Start with `references/app-scaffolding-wiring.md` to wire TabView + NavigationStack + sheets.
- Add a minimal `AppTab` and `RouterPath` based on the provided skeletons.
- Choose the next component reference based on the UI you need first (TabView, NavigationStack, Sheets).
- Expand the route and sheet enums as new screens are added.
## General rules to follow
- Use modern SwiftUI state (`@State`, `@Binding`, `@Observable`, `@Environment`) and avoid unnecessary view models.
- Prefer composition; keep views small and focused.
- Use async/await with `.task` and explicit loading/error states.
- Maintain existing legacy patterns only when editing legacy files.
- Follow the project's formatter and style guide.
- **Sheets**: Prefer `.sheet(item:)` over `.sheet(isPresented:)` when state represents a selected model. Avoid `if let` inside a sheet body. Sheets should own their actions and call `dismiss()` internally instead of forwarding `onCancel`/`onConfirm` closures.
## Workflow for a new SwiftUI view
1. Define the view's state and its ownership location.
2. Identify dependencies to inject via `@Environment`.
3. Sketch the view hierarchy and extract repeated parts into subviews.
4. Implement async loading with `.task` and explicit state enum if needed.
5. Add accessibility labels or identifiers when the UI is interactive.
6. Validate with a build and update usage callsites if needed.
## Component references
Use `references/components-index.md` as the entry point. Each component reference should include:
- Intent and best-fit scenarios.
- Minimal usage pattern with local conventions.
- Pitfalls and performance notes.
- Paths to existing examples in the current repo.
## Sheet patterns
### Item-driven sheet (preferred)
```swift
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
```
### Sheet owns its actions
```swift
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()
}
}
```
## Adding a new component reference
- Create `references/<component>.md`.
- Keep it short and actionable; link to concrete files in the current repo.
- Update `references/components-index.md` with the new entry.

View 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.

View file

@ -0,0 +1,43 @@
# Components Index
Use this file to find component-specific guidance. Each entry lists when to use it.
## Available components
- 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 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.
- macOS Settings: `references/macos-settings.md` — Use when building a macOS Settings window with SwiftUI's Settings scene.
- 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.
- ScrollView and Lazy stacks: `references/scrollview.md` — Use for custom layouts, horizontal scrollers, or grids.
- Grids: `references/grids.md` — Use for icon pickers, media galleries, and tiled layouts.
- Theming and dynamic type: `references/theming.md` — Use for app-wide theme tokens, colors, and type scaling.
- Controls (toggles, pickers, sliders): `references/controls.md` — Use for settings controls and input selection.
- Input toolbar (bottom anchored): `references/input-toolbar.md` — Use for chat/composer screens with a sticky input bar.
- Top bar overlays (iOS 26+ and fallback): `references/top-bar.md` — Use for pinned selectors or pills above scroll content.
- Overlay and toasts: `references/overlay.md` — Use for transient UI like banners or toasts.
- Focus handling: `references/focus.md` — Use for chaining fields and keyboard focus management.
- Searchable: `references/searchable.md` — Use for native search UI with scopes and async results.
- Async images and media: `references/media.md` — Use for remote media, previews, and media viewers.
- Haptics: `references/haptics.md` — Use for tactile feedback tied to key actions.
- 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.
- Menu bar commands: `references/menu-bar.md` — Use when adding or customizing macOS/iPadOS menu bar commands.
- Loading & placeholders: `references/loading-placeholders.md` — Use for redacted skeletons, empty states, and loading UX.
- Lightweight clients: `references/lightweight-clients.md` — Use for small, closure-based API clients injected into stores.
## Planned components (create files as needed)
- Web content: create `references/webview.md` — Use for embedded web content or in-app browsing.
- Status composer patterns: create `references/composer.md` — Use for composition or editor workflows.
- Text input and validation: create `references/text-input.md` — Use for forms, validation, and text-heavy input.
- Design system usage: create `references/design-system.md` — Use when applying shared styling rules.
## Adding entries
- Add the component file and link it here with a short “when to use” description.
- Keep each component reference short and actionable.

View file

@ -0,0 +1,57 @@
# Controls (Toggle, Slider, Picker)
## Intent
Use native controls for settings and configuration screens, keeping labels accessible and state bindings clear.
## Core patterns
- Bind controls directly to `@State`, `@Binding`, or `@AppStorage`.
- Prefer `Toggle` for boolean preferences.
- Use `Slider` for numeric ranges and show the current value in a label.
- Use `Picker` for discrete choices; use `.pickerStyle(.segmented)` only for 24 options.
- Keep labels visible and descriptive; avoid embedding buttons inside controls.
## Example: toggles with sections
```swift
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $preferences.notificationsMentionsEnabled)
Toggle("Follows", isOn: $preferences.notificationsFollowsEnabled)
Toggle("Boosts", isOn: $preferences.notificationsBoostsEnabled)
}
}
```
## Example: slider with value text
```swift
Section("Font Size") {
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
Text("Scale: \(String(format: \"%.1f\", fontSizeScale))")
.font(.scaledBody)
}
```
## Example: picker for enums
```swift
Picker("Default Visibility", selection: $visibility) {
ForEach(Visibility.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}
```
## Design choices to keep
- Group related controls in a `Form` section.
- Use `.disabled(...)` to reflect locked or inherited settings.
- Use `Label` inside toggles to combine icon + text when it adds clarity.
## Pitfalls
- Avoid `.pickerStyle(.segmented)` for large sets; use menu or inline styles instead.
- Dont hide labels for sliders; always show context.
- Avoid hard-coding colors for controls; use theme tint sparingly.

View file

@ -0,0 +1,66 @@
# Deep links and navigation
## Intent
Route external URLs into in-app destinations while falling back to system handling when needed.
## Core patterns
- Centralize URL handling in the router (`handle(url:)`, `handleDeepLink(url:)`).
- Inject an `OpenURLAction` handler that delegates to the router.
- Use `.onOpenURL` for app scheme links and convert them to web URLs if needed.
- Let the router decide whether to navigate or open externally.
## Example: router entry points
```swift
@MainActor
final class RouterPath {
var path: [Route] = []
var urlHandler: ((URL) -> OpenURLAction.Result)?
func handle(url: URL) -> OpenURLAction.Result {
if isInternal(url) {
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
return urlHandler?(url) ?? .systemAction
}
func handleDeepLink(url: URL) -> OpenURLAction.Result {
// Resolve federated URLs, then navigate.
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
}
```
## Example: attach to a root view
```swift
extension View {
func withLinkRouter(_ router: RouterPath) -> some View {
self
.environment(
\.openURL,
OpenURLAction { url in
router.handle(url: url)
}
)
.onOpenURL { url in
router.handleDeepLink(url: url)
}
}
}
```
## Design choices to keep
- Keep URL parsing and decision logic inside the router.
- Avoid handling deep links in multiple places; one entry point is enough.
- Always provide a fallback to `OpenURLAction` or `UIApplication.shared.open`.
## Pitfalls
- Dont assume the URL is internal; validate first.
- Avoid blocking UI while resolving remote links; use `Task`.

View file

@ -0,0 +1,90 @@
# Focus handling and field chaining
## Intent
Use `@FocusState` to control keyboard focus, chain fields, and coordinate focus across complex forms.
## Core patterns
- Use an enum to represent focusable fields.
- Set initial focus in `onAppear`.
- Use `.onSubmit` to move focus to the next field.
- For dynamic lists of fields, use an enum with associated values (e.g., `.option(Int)`).
## Example: single field focus
```swift
struct AddServerView: View {
@State private var server = ""
@FocusState private var isServerFieldFocused: Bool
var body: some View {
Form {
TextField("Server", text: $server)
.focused($isServerFieldFocused)
}
.onAppear { isServerFieldFocused = true }
}
}
```
## Example: chained focus with enum
```swift
struct EditTagView: View {
enum FocusField { case title, symbol, newTag }
@FocusState private var focusedField: FocusField?
var body: some View {
Form {
TextField("Title", text: $title)
.focused($focusedField, equals: .title)
.onSubmit { focusedField = .symbol }
TextField("Symbol", text: $symbol)
.focused($focusedField, equals: .symbol)
.onSubmit { focusedField = .newTag }
}
.onAppear { focusedField = .title }
}
}
```
## Example: dynamic focus for variable fields
```swift
struct PollView: View {
enum FocusField: Hashable { case option(Int) }
@FocusState private var focused: FocusField?
@State private var options: [String] = ["", ""]
@State private var currentIndex = 0
var body: some View {
ForEach(options.indices, id: \.self) { index in
TextField("Option \(index + 1)", text: $options[index])
.focused($focused, equals: .option(index))
.onSubmit { addOption(at: index) }
}
.onAppear { focused = .option(0) }
}
private func addOption(at index: Int) {
options.append("")
currentIndex = index + 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
focused = .option(currentIndex)
}
}
}
```
## Design choices to keep
- Keep focus state local to the view that owns the fields.
- Use focus changes to drive UX (validation messages, helper UI).
- Pair with `.scrollDismissesKeyboard(...)` when using ScrollView/Form.
## Pitfalls
- Dont store focus state in shared objects; it is view-local.
- Avoid aggressive focus changes during animation; delay if needed.

View file

@ -0,0 +1,97 @@
# Form
## Intent
Use `Form` for structured settings, grouped inputs, and action rows. This pattern keeps layout, spacing, and accessibility consistent for data entry screens.
## Core patterns
- Wrap the form in a `NavigationStack` only when it is presented in a sheet or standalone view without an existing navigation context.
- Group related controls into `Section` blocks.
- Use `.scrollContentBackground(.hidden)` plus a custom background color when you need design-system colors.
- Apply `.formStyle(.grouped)` for grouped styling when appropriate.
- Use `@FocusState` to manage keyboard focus in input-heavy forms.
## Example: settings-style form
```swift
@MainActor
struct SettingsView: View {
@Environment(Theme.self) private var theme
var body: some View {
NavigationStack {
Form {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Edit profile") { /* open sheet */ }
.buttonStyle(.plain)
}
.listRowBackground(theme.primaryBackgroundColor)
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
}
}
```
## Example: modal form with validation
```swift
@MainActor
struct AddRemoteServerView: View {
@Environment(\.dismiss) private var dismiss
@Environment(Theme.self) private var theme
@State private var server: String = ""
@State private var isValid = false
@FocusState private var isServerFieldFocused: Bool
var body: some View {
NavigationStack {
Form {
TextField("Server URL", text: $server)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($isServerFieldFocused)
.listRowBackground(theme.primaryBackgroundColor)
Button("Add") {
guard isValid else { return }
dismiss()
}
.disabled(!isValid)
.listRowBackground(theme.primaryBackgroundColor)
}
.formStyle(.grouped)
.navigationTitle("Add Server")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.toolbar { CancelToolbarItem() }
.onAppear { isServerFieldFocused = true }
}
}
}
```
## Design choices to keep
- Prefer `Form` over custom stacks for settings and input screens.
- Keep rows tappable by using `.contentShape(Rectangle())` and `.buttonStyle(.plain)` on row buttons.
- Use list row backgrounds to keep section styling consistent with your theme.
## Pitfalls
- Avoid heavy custom layouts inside a `Form`; it can lead to spacing issues.
- If you need highly custom layouts, prefer `ScrollView` + `VStack`.
- Dont mix multiple background strategies; pick either default Form styling or custom colors.

View file

@ -0,0 +1,71 @@
# Grids
## Intent
Use `LazyVGrid` for icon pickers, media galleries, and dense visual selections where items align in columns.
## Core patterns
- Use `.adaptive` columns for layouts that should scale across device sizes.
- Use multiple `.flexible` columns when you want a fixed column count.
- Keep spacing consistent and small to avoid uneven gutters.
- Use `GeometryReader` inside grid cells when you need square thumbnails.
## Example: adaptive icon grid
```swift
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns, spacing: 6) {
ForEach(icons) { icon in
Button {
select(icon)
} label: {
ZStack(alignment: .bottomTrailing) {
Image(icon.previewName)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(6)
if icon.isSelected {
Image(systemName: "checkmark.seal.fill")
.padding(4)
.tint(.green)
}
}
}
.buttonStyle(.plain)
}
}
```
## Example: fixed 3-column media grid
```swift
LazyVGrid(
columns: [
.init(.flexible(minimum: 100), spacing: 4),
.init(.flexible(minimum: 100), spacing: 4),
.init(.flexible(minimum: 100), spacing: 4),
],
spacing: 4
) {
ForEach(items) { item in
GeometryReader { proxy in
ThumbnailView(item: item)
.frame(width: proxy.size.width, height: proxy.size.width)
}
.aspectRatio(1, contentMode: .fit)
}
}
```
## Design choices to keep
- Use `LazyVGrid` for large collections; avoid non-lazy grids for big sets.
- Keep tap targets full-bleed using `.contentShape(Rectangle())` when needed.
- Prefer adaptive grids for settings pickers and flexible layouts.
## Pitfalls
- Avoid heavy overlays in every grid cell; it can be expensive.
- Dont nest grids inside other grids without a clear reason.

View file

@ -0,0 +1,71 @@
# Haptics
## Intent
Use haptics sparingly to reinforce user actions (tab selection, refresh, success/error) and respect user preferences.
## Core patterns
- Centralize haptic triggers in a `HapticManager` or similar utility.
- Gate haptics behind user preferences and hardware support.
- Use distinct types for different UX moments (selection vs. notification vs. refresh).
## Example: simple haptic manager
```swift
@MainActor
final class HapticManager {
static let shared = HapticManager()
enum HapticType {
case buttonPress
case tabSelection
case dataRefresh(intensity: CGFloat)
case notification(UINotificationFeedbackGenerator.FeedbackType)
}
private let selectionGenerator = UISelectionFeedbackGenerator()
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
private let notificationGenerator = UINotificationFeedbackGenerator()
private init() { selectionGenerator.prepare() }
func fire(_ type: HapticType, isEnabled: Bool) {
guard isEnabled else { return }
switch type {
case .buttonPress:
impactGenerator.impactOccurred()
case .tabSelection:
selectionGenerator.selectionChanged()
case let .dataRefresh(intensity):
impactGenerator.impactOccurred(intensity: intensity)
case let .notification(style):
notificationGenerator.notificationOccurred(style)
}
}
}
```
## Example: usage
```swift
Button("Save") {
HapticManager.shared.fire(.notification(.success), isEnabled: preferences.hapticsEnabled)
}
TabView(selection: $selectedTab) { /* tabs */ }
.onChange(of: selectedTab) { _, _ in
HapticManager.shared.fire(.tabSelection, isEnabled: preferences.hapticTabSelectionEnabled)
}
```
## Design choices to keep
- Haptics should be subtle and not fire on every tiny interaction.
- Respect user preferences (toggle to disable).
- Keep haptic triggers close to the user action, not deep in data layers.
## Pitfalls
- Avoid firing multiple haptics in quick succession.
- Do not assume haptics are available; check support.

View file

@ -0,0 +1,51 @@
# Input toolbar (bottom anchored)
## Intent
Use a bottom-anchored input bar for chat, composer, or quick actions without fighting the keyboard.
## Core patterns
- Use `.safeAreaInset(edge: .bottom)` to anchor the toolbar above the keyboard.
- Keep the main content in a `ScrollView` or `List`.
- Drive focus with `@FocusState` and set initial focus when needed.
- Avoid embedding the input bar inside the scroll content; keep it separate.
## Example: scroll view + bottom input
```swift
@MainActor
struct ConversationView: View {
@FocusState private var isInputFocused: Bool
var body: some View {
ScrollViewReader { _ in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
}
}
.padding(.horizontal, .layoutPadding)
}
.safeAreaInset(edge: .bottom) {
InputBar(text: $draft)
.focused($isInputFocused)
}
.scrollDismissesKeyboard(.interactively)
.onAppear { isInputFocused = true }
}
}
}
```
## Design choices to keep
- Keep the input bar visually separated from the scrollable content.
- Use `.scrollDismissesKeyboard(.interactively)` for chat-like screens.
- Ensure send actions are reachable via keyboard return or a clear button.
## Pitfalls
- Avoid placing the input view inside the scroll stack; it will jump with content.
- Avoid nested scroll views that fight for drag gestures.

View file

@ -0,0 +1,93 @@
# Lightweight Clients (Closure-Based)
Use this pattern to keep networking or service dependencies simple and testable without introducing a full view model or heavy DI framework. It works well for SwiftUI apps where you want a small, composable API surface that can be swapped in previews/tests.
## Intent
- Provide a tiny "client" type made of async closures.
- Keep business logic in a store or feature layer, not the view.
- Enable easy stubbing in previews/tests.
## Minimal shape
```swift
struct SomeClient {
var fetchItems: (_ limit: Int) async throws -> [Item]
var search: (_ query: String, _ limit: Int) async throws -> [Item]
}
extension SomeClient {
static func live(baseURL: URL = URL(string: "https://example.com")!) -> SomeClient {
let session = URLSession.shared
return SomeClient(
fetchItems: { limit in
// build URL, call session, decode
},
search: { query, limit in
// build URL, call session, decode
}
)
}
}
```
## Usage pattern
```swift
@MainActor
@Observable final class ItemsStore {
enum LoadState { case idle, loading, loaded, failed(String) }
var items: [Item] = []
var state: LoadState = .idle
private let client: SomeClient
init(client: SomeClient) {
self.client = client
}
func load(limit: Int = 20) async {
state = .loading
do {
items = try await client.fetchItems(limit)
state = .loaded
} catch {
state = .failed(error.localizedDescription)
}
}
}
```
```swift
struct ContentView: View {
@Environment(ItemsStore.self) private var store
var body: some View {
List(store.items) { item in
Text(item.title)
}
.task { await store.load() }
}
}
```
```swift
@main
struct MyApp: App {
@State private var store = ItemsStore(client: .live())
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
}
}
}
```
## Guidance
- Keep decoding and URL-building in the client; keep state changes in the store.
- Make the store accept the client in `init` and keep it private.
- Avoid global singletons; use `.environment` for store injection.
- If you need multiple variants (mock/stub), add `static func mock(...)`.
## Pitfalls
- Dont put UI state in the client; keep state in the store.
- Dont capture `self` or view state in the client closures.

View file

@ -0,0 +1,86 @@
# List and Section
## Intent
Use `List` for feed-style content and settings-style rows where built-in row reuse, selection, and accessibility matter.
## Core patterns
- Prefer `List` for long, vertically scrolling content with repeated rows.
- Use `Section` headers to group related rows.
- Pair with `ScrollViewReader` when you need scroll-to-top or jump-to-id.
- Use `.listStyle(.plain)` for modern feed layouts.
- Use `.listStyle(.grouped)` for multi-section discovery/search pages where section grouping helps.
- Apply `.scrollContentBackground(.hidden)` + a custom background when you need a themed surface.
- Use `.listRowInsets(...)` and `.listRowSeparator(.hidden)` to tune row spacing and separators.
- Use `.environment(\\.defaultMinListRowHeight, ...)` to control dense list layouts.
## Example: feed list with scroll-to-top
```swift
@MainActor
struct TimelineListView: View {
@Environment(\.selectedTabScrollToTop) private var selectedTabScrollToTop
@State private var scrollToId: String?
var body: some View {
ScrollViewReader { proxy in
List {
ForEach(items) { item in
TimelineRow(item: item)
.id(item.id)
.listRowInsets(.init(top: 12, leading: 16, bottom: 6, trailing: 16))
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
.environment(\\.defaultMinListRowHeight, 1)
.onChange(of: scrollToId) { _, newValue in
if let newValue {
proxy.scrollTo(newValue, anchor: .top)
scrollToId = nil
}
}
.onChange(of: selectedTabScrollToTop) { _, newValue in
if newValue == 0 {
withAnimation {
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
}
}
}
}
}
}
```
## Example: settings-style list
```swift
@MainActor
struct SettingsView: View {
var body: some View {
List {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Sign Out", role: .destructive) {}
}
}
.listStyle(.insetGrouped)
}
}
```
## Design choices to keep
- Use `List` for dynamic feeds, settings, and any UI where row semantics help.
- Use stable IDs for rows to keep animations and scroll positioning reliable.
- Prefer `.contentShape(Rectangle())` on rows that should be tappable end-to-end.
- Use `.refreshable` for pull-to-refresh feeds when the data source supports it.
## Pitfalls
- Avoid heavy custom layouts inside a `List` row; use `ScrollView` + `LazyVStack` instead.
- Be careful mixing `List` and nested `ScrollView`; it can cause gesture conflicts.

View file

@ -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
- Dont animate layout shifts during redaction; keep frames stable.
- Avoid nesting multiple spinners; use one loading indicator per section.
- Keep placeholder count small (36) 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) }
}
}
```

View file

@ -0,0 +1,71 @@
# macOS Settings
## Intent
Use this when building a macOS Settings window backed by SwiftUI's `Settings` scene.
## Core patterns
- Declare the Settings scene in the `App` and compile it only for macOS.
- Keep settings content in a dedicated root view (`SettingsView`) and drive values with `@AppStorage`.
- Use `TabView` to group settings sections when you have more than one category.
- Use `Form` inside each tab to keep controls aligned and accessible.
- Use `OpenSettingsAction` or `SettingsLink` for in-app entry points to the Settings window.
## Example: settings scene
```swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
#if os(macOS)
Settings {
SettingsView()
}
#endif
}
}
```
## Example: tabbed settings view
```swift
@MainActor
struct SettingsView: View {
@AppStorage("showPreviews") private var showPreviews = true
@AppStorage("fontSize") private var fontSize = 12.0
var body: some View {
TabView {
Form {
Toggle("Show Previews", isOn: $showPreviews)
Slider(value: $fontSize, in: 9...96) {
Text("Font Size (\(fontSize, specifier: "%.0f") pts)")
}
}
.tabItem { Label("General", systemImage: "gear") }
Form {
Toggle("Enable Advanced Mode", isOn: .constant(false))
}
.tabItem { Label("Advanced", systemImage: "star") }
}
.scenePadding()
.frame(maxWidth: 420, minHeight: 240)
}
}
```
## Skip navigation
- Avoid wrapping `SettingsView` in a `NavigationStack` unless you truly need deep push navigation.
- Prefer tabs or sections; Settings is already presented as a separate window and should feel flat.
- If you must show hierarchical settings, use a single `NavigationSplitView` with a sidebar list of categories.
## Pitfalls
- Dont reuse iOS-only settings layouts (full-screen stacks, toolbar-heavy flows).
- Avoid large custom view hierarchies inside `Form`; keep rows focused and accessible.

View file

@ -0,0 +1,59 @@
# Matched transitions
## Intent
Use matched transitions to create smooth continuity between a source view (thumbnail, avatar) and a destination view (sheet, detail, viewer).
## Core patterns
- Use a shared `Namespace` and a stable ID for the source.
- Use `matchedTransitionSource` + `navigationTransition(.zoom(...))` on iOS 26+.
- Use `matchedGeometryEffect` for in-place transitions within a view hierarchy.
- Keep IDs stable across view updates (avoid random UUIDs).
## Example: media preview to full-screen viewer (iOS 26+)
```swift
struct MediaPreview: View {
@Namespace private var namespace
@State private var selected: MediaAttachment?
var body: some View {
ThumbnailView()
.matchedTransitionSource(id: selected?.id ?? "", in: namespace)
.sheet(item: $selected) { item in
MediaViewer(item: item)
.navigationTransition(.zoom(sourceID: item.id, in: namespace))
}
}
}
```
## Example: matched geometry within a view
```swift
struct ToggleBadge: View {
@Namespace private var space
@State private var isOn = false
var body: some View {
Button {
withAnimation(.spring) { isOn.toggle() }
} label: {
Image(systemName: isOn ? "eye" : "eye.slash")
.matchedGeometryEffect(id: "icon", in: space)
}
}
}
```
## Design choices to keep
- Prefer `matchedTransitionSource` for cross-screen transitions.
- Keep source and destination sizes reasonable to avoid jarring scale changes.
- Use `withAnimation` for state-driven transitions.
## Pitfalls
- Dont use unstable IDs; it breaks the transition.
- Avoid mismatched shapes (e.g., square to circle) unless the design expects it.

View file

@ -0,0 +1,73 @@
# Media (images, video, viewer)
## Intent
Use consistent patterns for loading images, previewing media, and presenting a full-screen viewer.
## Core patterns
- Use `LazyImage` (or `AsyncImage`) for remote images with loading states.
- Prefer a lightweight preview component for inline media.
- Use a shared viewer state (e.g., `QuickLook`) to present a full-screen media viewer.
- Use `openWindow` for desktop/visionOS and a sheet for iOS.
## Example: inline media preview
```swift
struct MediaPreviewRow: View {
@Environment(QuickLook.self) private var quickLook
let attachments: [MediaAttachment]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(attachments) { attachment in
LazyImage(url: attachment.previewURL) { state in
if let image = state.image {
image.resizable().aspectRatio(contentMode: .fill)
} else {
ProgressView()
}
}
.frame(width: 120, height: 120)
.clipped()
.onTapGesture {
quickLook.prepareFor(
selectedMediaAttachment: attachment,
mediaAttachments: attachments
)
}
}
}
}
}
}
```
## Example: global media viewer sheet
```swift
struct AppRoot: View {
@State private var quickLook = QuickLook.shared
var body: some View {
content
.environment(quickLook)
.sheet(item: $quickLook.selectedMediaAttachment) { selected in
MediaUIView(selectedAttachment: selected, attachments: quickLook.mediaAttachments)
}
}
}
```
## Design choices to keep
- Keep previews lightweight; load full media in the viewer.
- Use shared viewer state so any view can open media without prop-drilling.
- Use a single entry point for the viewer (sheet/window) to avoid duplicates.
## Pitfalls
- Avoid loading full-size images in list rows; use resized previews.
- Dont present multiple viewer sheets at once; keep a single source of truth.

View file

@ -0,0 +1,101 @@
# Menu Bar
## Intent
Use this when adding or customizing the macOS/iPadOS menu bar with SwiftUI commands.
## Core patterns
- Add commands at the `Scene` level with `.commands { ... }`.
- Use `SidebarCommands()` when your UI includes a navigation sidebar.
- Use `CommandMenu` for app-specific menus and group related actions.
- Use `CommandGroup` to insert items before/after system groups or replace them.
- Use `FocusedValue` for context-sensitive menu items that depend on the active scene.
## Example: basic command menu
```swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("Actions") {
Button("Run", action: run)
.keyboardShortcut("R")
Button("Stop", action: stop)
.keyboardShortcut(".")
}
}
}
private func run() {}
private func stop() {}
}
```
## Example: insert and replace groups
```swift
WindowGroup {
ContentView()
}
.commands {
CommandGroup(before: .systemServices) {
Button("Check for Updates") { /* open updater */ }
}
CommandGroup(after: .newItem) {
Button("New from Clipboard") { /* create item */ }
}
CommandGroup(replacing: .help) {
Button("User Manual") { /* open docs */ }
}
}
```
## Example: focused menu state
```swift
@Observable
final class DataModel {
var items: [String] = []
}
struct ContentView: View {
@State private var model = DataModel()
var body: some View {
List(model.items, id: \.self) { item in
Text(item)
}
.focusedSceneValue(model)
}
}
struct ItemCommands: Commands {
@FocusedValue(DataModel.self) private var model: DataModel?
var body: some Commands {
CommandGroup(after: .newItem) {
Button("New Item") {
model?.items.append("Untitled")
}
.disabled(model == nil)
}
}
}
```
## Menu bar and Settings
- Defining a `Settings` scene adds the Settings menu item on macOS automatically.
- If you need a custom entry point inside the app, use `OpenSettingsAction` or `SettingsLink`.
## Pitfalls
- Avoid registering the same keyboard shortcut in multiple command groups.
- Dont use menu items as the only discoverable entry point for critical features.

View file

@ -0,0 +1,159 @@
# NavigationStack
## Intent
Use this pattern for programmatic navigation and deep links, especially when each tab needs an independent navigation history. The key idea is one `NavigationStack` per tab, each with its own path binding and router object.
## Core architecture
- Define a route enum that is `Hashable` and represents all destinations.
- Create a lightweight router (or use a library such as `https://github.com/Dimillian/AppRouter`) that owns the `path` and any sheet state.
- Each tab owns its own router instance and binds `NavigationStack(path:)` to it.
- Inject the router into the environment so child views can navigate programmatically.
- Centralize destination mapping with a single `navigationDestination(for:)` block (or a `withAppRouter()` modifier).
## Example: custom router with per-tab stack
```swift
@MainActor
@Observable
final class RouterPath {
var path: [Route] = []
var presentedSheet: SheetDestination?
func navigate(to route: Route) {
path.append(route)
}
func reset() {
path = []
}
}
enum Route: Hashable {
case account(id: String)
case status(id: String)
}
@MainActor
struct TimelineTab: View {
@State private var routerPath = RouterPath()
var body: some View {
NavigationStack(path: $routerPath.path) {
TimelineView()
.navigationDestination(for: Route.self) { route in
switch route {
case .account(let id): AccountView(id: id)
case .status(let id): StatusView(id: id)
}
}
}
.environment(routerPath)
}
}
```
## Example: centralized destination mapping
Use a shared view modifier to avoid duplicating route switches across screens.
```swift
extension View {
func withAppRouter() -> some View {
navigationDestination(for: Route.self) { route in
switch route {
case .account(let id):
AccountView(id: id)
case .status(let id):
StatusView(id: id)
}
}
}
}
```
Then apply it once per stack:
```swift
NavigationStack(path: $routerPath.path) {
TimelineView()
.withAppRouter()
}
```
## Example: binding per tab (tabs with independent history)
```swift
@MainActor
struct TabsView: View {
@State private var timelineRouter = RouterPath()
@State private var notificationsRouter = RouterPath()
var body: some View {
TabView {
TimelineTab(router: timelineRouter)
NotificationsTab(router: notificationsRouter)
}
}
}
```
## Example: generic tabs with per-tab NavigationStack
Use this when tabs are built from data and each needs its own path without hard-coded names.
```swift
@MainActor
struct TabsView: View {
@State private var selectedTab: AppTab = .timeline
@State private var tabRouter = TabRouter()
var body: some View {
TabView(selection: $selectedTab) {
ForEach(AppTab.allCases) { tab in
NavigationStack(path: tabRouter.binding(for: tab)) {
tab.makeContentView()
}
.environment(tabRouter.router(for: tab))
.tabItem { tab.label }
.tag(tab)
}
}
}
}
```
@MainActor
@Observable
final class TabRouter {
private var routers: [AppTab: RouterPath] = [:]
func router(for tab: AppTab) -> RouterPath {
if let router = routers[tab] { return router }
let router = RouterPath()
routers[tab] = router
return router
}
func binding(for tab: AppTab) -> Binding<[Route]> {
let router = router(for: tab)
return Binding(get: { router.path }, set: { router.path = $0 })
}
}
## Design choices to keep
- One `NavigationStack` per tab to preserve independent history.
- A single source of truth for navigation state (`RouterPath` or library router).
- Use `navigationDestination(for:)` to map routes to views.
- Reset the path when app context changes (account switch, logout, etc.).
- Inject the router into the environment so child views can navigate and present sheets without prop-drilling.
- Keep sheet presentation state on the router if you want a single place to manage modals.
## Pitfalls
- Do not share one path across all tabs unless you want global history.
- Ensure route identifiers are stable and `Hashable`.
- Avoid storing view instances in the path; store lightweight route data instead.
- If using a router object, keep it outside other `@Observable` objects to avoid nested observation.

View file

@ -0,0 +1,45 @@
# Overlay and toasts
## Intent
Use overlays for transient UI (toasts, banners, loaders) without affecting layout.
## Core patterns
- Use `.overlay(alignment:)` to place global UI without changing the underlying layout.
- Keep overlays lightweight and dismissible.
- Use a dedicated `ToastCenter` (or similar) for global state if multiple features trigger toasts.
## Example: toast overlay
```swift
struct AppRootView: View {
@State private var toast: Toast?
var body: some View {
content
.overlay(alignment: .top) {
if let toast {
ToastView(toast: toast)
.transition(.move(edge: .top).combined(with: .opacity))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation { self.toast = nil }
}
}
}
}
}
}
```
## Design choices to keep
- Prefer overlays for transient UI rather than embedding in layout stacks.
- Use transitions and short auto-dismiss timers.
- Keep the overlay aligned to a clear edge (`.top` or `.bottom`).
## Pitfalls
- Avoid overlays that block all interaction unless explicitly needed.
- Dont stack many overlays; use a queue or replace the current toast.

View file

@ -0,0 +1,87 @@
# ScrollView and Lazy stacks
## Intent
Use `ScrollView` with `LazyVStack`, `LazyHStack`, or `LazyVGrid` when you need custom layout, mixed content, or horizontal/ grid-based scrolling.
## Core patterns
- Prefer `ScrollView` + `LazyVStack` for chat-like or custom feed layouts.
- Use `ScrollView(.horizontal)` + `LazyHStack` for chips, tags, avatars, and media strips.
- Use `LazyVGrid` for icon/media grids; prefer adaptive columns when possible.
- Use `ScrollViewReader` for scroll-to-top/bottom and anchor-based jumps.
- Use `safeAreaInset(edge:)` for input bars that should stick above the keyboard.
## Example: vertical custom feed
```swift
@MainActor
struct ConversationView: View {
private enum Constants { static let bottomAnchor = "bottom" }
@State private var scrollProxy: ScrollViewProxy?
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
.id(message.id)
}
Color.clear.frame(height: 1).id(Constants.bottomAnchor)
}
.padding(.horizontal, .layoutPadding)
}
.safeAreaInset(edge: .bottom) {
MessageInputBar()
}
.onAppear {
scrollProxy = proxy
withAnimation {
proxy.scrollTo(Constants.bottomAnchor, anchor: .bottom)
}
}
}
}
}
```
## Example: horizontal chips
```swift
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
```
## Example: adaptive grid
```swift
let columns = [GridItem(.adaptive(minimum: 120))]
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(items) { item in
GridItemView(item: item)
}
}
.padding(8)
}
```
## Design choices to keep
- Use `Lazy*` stacks when item counts are large or unknown.
- Use non-lazy stacks for small, fixed-size content to avoid lazy overhead.
- Keep IDs stable when using `ScrollViewReader`.
- Prefer explicit animations (`withAnimation`) when scrolling to an ID.
## Pitfalls
- Avoid nesting scroll views of the same axis; it causes gesture conflicts.
- Dont combine `List` and `ScrollView` in the same hierarchy without a clear reason.
- Overuse of `LazyVStack` for tiny content can add unnecessary complexity.

View file

@ -0,0 +1,71 @@
# Searchable
## Intent
Use `searchable` to add native search UI with optional scopes and async results.
## Core patterns
- Bind `searchable(text:)` to local state.
- Use `.searchScopes` for multiple search modes.
- Use `.task(id: searchQuery)` or debounced tasks to avoid overfetching.
- Show placeholders or progress states while results load.
## Example: searchable with scopes
```swift
@MainActor
struct ExploreView: View {
@State private var searchQuery = ""
@State private var searchScope: SearchScope = .all
@State private var isSearching = false
@State private var results: [SearchResult] = []
var body: some View {
List {
if isSearching {
ProgressView()
} else {
ForEach(results) { result in
SearchRow(result: result)
}
}
}
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("Search")
)
.searchScopes($searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.title)
}
}
.task(id: searchQuery) {
await runSearch()
}
}
private func runSearch() async {
guard !searchQuery.isEmpty else {
results = []
return
}
isSearching = true
defer { isSearching = false }
try? await Task.sleep(for: .milliseconds(250))
results = await fetchResults(query: searchQuery, scope: searchScope)
}
}
```
## Design choices to keep
- Show a placeholder when search is empty or has no results.
- Debounce input to avoid spamming the network.
- Keep search state local to the view.
## Pitfalls
- Avoid running searches for empty strings.
- Dont block the main thread during fetch.

View file

@ -0,0 +1,113 @@
# 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 `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
```swift
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
```swift
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
```swift
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.
```swift
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.
## 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.

View file

@ -0,0 +1,72 @@
# Split views and columns
## Intent
Provide a lightweight, customizable multi-column layout for iPad/macOS without relying on `NavigationSplitView`.
## Custom split column pattern (manual HStack)
Use this when you want full control over column sizing, behavior, and environment tweaks.
```swift
@MainActor
struct AppView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@AppStorage("showSecondaryColumn") private var showSecondaryColumn = true
var body: some View {
HStack(spacing: 0) {
primaryColumn
if shouldShowSecondaryColumn {
Divider().edgesIgnoringSafeArea(.all)
secondaryColumn
}
}
}
private var shouldShowSecondaryColumn: Bool {
horizontalSizeClass == .regular
&& showSecondaryColumn
}
private var primaryColumn: some View {
TabView { /* tabs */ }
}
private var secondaryColumn: some View {
NotificationsTab()
.environment(\.isSecondaryColumn, true)
.frame(maxWidth: .secondaryColumnWidth)
}
}
```
## Notes on the custom approach
- Use a shared preference or setting to toggle the secondary column.
- Inject an environment flag (e.g., `isSecondaryColumn`) so child views can adapt behavior.
- Prefer a fixed or capped width for the secondary column to avoid layout thrash.
## Alternative: NavigationSplitView
`NavigationSplitView` can handle sidebar + detail + supplementary columns for you, but is harder to customize in cases like:\n- a dedicated notification column independent of selection,\n- custom sizing, or\n- different toolbar behaviors per column.
```swift
@MainActor
struct AppView: View {
var body: some View {
NavigationSplitView {
SidebarView()
} content: {
MainContentView()
} detail: {
NotificationsView()
}
}
}
```
## When to choose which
- Use the manual HStack split when you need full control or a non-standard secondary column.
- Use `NavigationSplitView` when you want a standard system layout with minimal customization.

View file

@ -0,0 +1,114 @@
# TabView
## Intent
Use this pattern for a scalable, multi-platform tab architecture with:
- a single source of truth for tab identity and content,
- platform-specific tab sets and sidebar sections,
- dynamic tabs sourced from data,
- an interception hook for special tabs (e.g., compose).
## Core architecture
- `AppTab` enum defines identity, labels, icons, and content builder.
- `SidebarSections` enum groups tabs for sidebar sections.
- `AppView` owns the `TabView` and selection binding, and routes tab changes through `updateTab`.
## Example: custom binding with side effects
Use this when tab selection needs side effects, like intercepting a special tab to perform an action instead of changing selection.
```swift
@MainActor
struct AppView: View {
@Binding var selectedTab: AppTab
var body: some View {
TabView(selection: .init(
get: { selectedTab },
set: { updateTab(with: $0) }
)) {
ForEach(availableSections) { section in
TabSection(section.title) {
ForEach(section.tabs) { tab in
Tab(value: tab) {
tab.makeContentView(
homeTimeline: $timeline,
selectedTab: $selectedTab,
pinnedFilters: $pinnedFilters
)
} label: {
tab.label
}
.tabPlacement(tab.tabPlacement)
}
}
.tabPlacement(.sidebarOnly)
}
}
}
private func updateTab(with newTab: AppTab) {
if newTab == .post {
// Intercept special tabs (compose) instead of changing selection.
presentComposer()
return
}
selectedTab = newTab
}
}
```
## Example: direct binding without side effects
Use this when selection is purely state-driven.
```swift
@MainActor
struct AppView: View {
@Binding var selectedTab: AppTab
var body: some View {
TabView(selection: $selectedTab) {
ForEach(availableSections) { section in
TabSection(section.title) {
ForEach(section.tabs) { tab in
Tab(value: tab) {
tab.makeContentView(
homeTimeline: $timeline,
selectedTab: $selectedTab,
pinnedFilters: $pinnedFilters
)
} label: {
tab.label
}
.tabPlacement(tab.tabPlacement)
}
}
.tabPlacement(.sidebarOnly)
}
}
}
}
```
## Design choices to keep
- Centralize tab identity and content in `AppTab` with `makeContentView(...)`.
- Use `Tab(value:)` with `selection` binding for state-driven tab selection.
- Route selection changes through `updateTab` to handle special tabs and scroll-to-top behavior.
- Use `TabSection` + `.tabPlacement(.sidebarOnly)` for sidebar structure.
- Use `.tabPlacement(.pinned)` in `AppTab.tabPlacement` for a single pinned tab; this is commonly used for iOS 26 `.searchable` tab content, but can be used for any tab.
## Dynamic tabs pattern
- `SidebarSections` handles dynamic data tabs.
- `AppTab.anyTimelineFilter(filter:)` wraps dynamic tabs in a single enum case.
- The enum provides label/icon/title for dynamic tabs via the filter type.
## Pitfalls
- Avoid adding ViewModels for tabs; keep state local or in `@Observable` services.
- Do not nest `@Observable` objects inside other `@Observable` objects.
- Ensure `AppTab.id` values are stable; dynamic cases should hash on stable IDs.
- Special tabs (compose) should not change selection.

View file

@ -0,0 +1,71 @@
# Theming and dynamic type
## Intent
Provide a clean, scalable theming approach that keeps view code semantic and consistent.
## Core patterns
- Use a single `Theme` object as the source of truth (colors, fonts, spacing).
- Inject theme at the app root and read it via `@Environment(Theme.self)` in views.
- Prefer semantic colors (`primaryBackground`, `secondaryBackground`, `label`, `tint`) instead of raw colors.
- Keep user-facing theme controls in a dedicated settings screen.
- Apply Dynamic Type scaling through custom fonts or `.font(.scaled...)`.
## Example: Theme object
```swift
@MainActor
@Observable
final class Theme {
var tintColor: Color = .blue
var primaryBackground: Color = .white
var secondaryBackground: Color = .gray.opacity(0.1)
var labelColor: Color = .primary
var fontSizeScale: Double = 1.0
}
```
## Example: inject at app root
```swift
@main
struct MyApp: App {
@State private var theme = Theme()
var body: some Scene {
WindowGroup {
AppView()
.environment(theme)
}
}
}
```
## Example: view usage
```swift
struct ProfileView: View {
@Environment(Theme.self) private var theme
var body: some View {
VStack {
Text("Profile")
.foregroundStyle(theme.labelColor)
}
.background(theme.primaryBackground)
}
}
```
## Design choices to keep
- Keep theme values semantic and minimal; avoid duplicating system colors.
- Store user-selected theme values in persistent storage if needed.
- Ensure contrast between text and backgrounds.
## Pitfalls
- Avoid sprinkling raw `Color` values in views; it breaks consistency.
- Do not tie theme to a single views local state.
- Avoid using `@Environment(\\.colorScheme)` as the only theme control; it should complement your theme.

View file

@ -0,0 +1,93 @@
# Title menus
## Intent
Use a title menu in the navigation bar to provide contextspecific filtering or quick actions without adding extra chrome.
## Core patterns
- Use `ToolbarTitleMenu` to attach a menu to the navigation title.
- Keep the menu content compact and grouped with dividers.
## Example: title menu for filters
```swift
@ToolbarContentBuilder
private var toolbarView: some ToolbarContent {
ToolbarTitleMenu {
Button("Latest") { timeline = .latest }
Button("Resume") { timeline = .resume }
Divider()
Button("Local") { timeline = .local }
Button("Federated") { timeline = .federated }
}
}
```
## Example: attach to a view
```swift
NavigationStack {
TimelineView()
.toolbar {
toolbarView
}
}
```
## Example: title + menu together
```swift
struct TimelineScreen: View {
@State private var timeline: TimelineFilter = .home
var body: some View {
NavigationStack {
TimelineView()
.toolbar {
ToolbarItem(placement: .principal) {
VStack(spacing: 2) {
Text(timeline.title)
.font(.headline)
Text(timeline.subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
ToolbarTitleMenu {
Button("Home") { timeline = .home }
Button("Local") { timeline = .local }
Button("Federated") { timeline = .federated }
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
}
```
## Example: title + subtitle with menu
```swift
ToolbarItem(placement: .principal) {
VStack(spacing: 2) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
```
## Design choices to keep
- Only show the title menu when filtering or context switching is available.
- Keep the title readable; avoid long labels that truncate.
- Use secondary text below the title if extra context is needed.
## Pitfalls
- Dont overload the menu with too many options.
- Avoid using title menus for destructive actions.

View file

@ -0,0 +1,49 @@
# Top bar overlays (iOS 26+ and fallback)
## Intent
Provide a custom top selector or pill row that sits above scroll content, using `safeAreaBar(.top)` on iOS 26 and a compatible fallback on earlier OS versions.
## iOS 26+ approach
Use `safeAreaBar(edge: .top)` to attach the view to the safe area bar.
```swift
if #available(iOS 26.0, *) {
content
.safeAreaBar(edge: .top) {
TopSelectorView()
.padding(.horizontal, .layoutPadding)
}
}
```
## Fallback for earlier iOS
Use `.safeAreaInset(edge: .top)` and hide the toolbar background to avoid double layers.
```swift
content
.toolbarBackground(.hidden, for: .navigationBar)
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
TopSelectorView()
.padding(.vertical, 8)
.padding(.horizontal, .layoutPadding)
.background(Color.primary.opacity(0.06))
.background(Material.ultraThin)
Divider()
}
}
```
## Design choices to keep
- Use `safeAreaBar` when available; it integrates better with the navigation bar.
- Use a subtle background + divider in the fallback to keep separation from content.
- Keep the selector height compact to avoid pushing content too far down.
## Pitfalls
- Dont stack multiple top insets; it can create extra padding.
- Avoid heavy, opaque backgrounds that fight the navigation bar.

View file

@ -8,9 +8,38 @@ A collection of specialized skills for iOS and Swift development workflows.
This repository contains a set of focused skills designed to assist with common iOS development tasks, from generating release notes to debugging apps and maintaining code quality.
Install: place these skill folders under `$CODEX_HOME/skills/public` (or symlink this repo there).
## Installation
Optional: enable the pre-commit hook to keep `docs/skills.json` in sync:
### Claude Code
For **project-level skills** (recommended), clone or symlink this repo and the skills in `.claude/skills/` will be automatically available:
```bash
# Clone into your project directory
git clone https://github.com/dimillian/Skills.git .claude-skills
# Or symlink specific skills
ln -s /path/to/Skills/.claude/skills/swiftui-ui-patterns .claude/skills/swiftui-ui-patterns
```
For **personal skills** (available across all projects):
```bash
# Copy skills to your personal Claude Code skills directory
cp -r .claude/skills/* ~/.claude/skills/
```
**Currently available for Claude Code:**
- `swiftui-ui-patterns` - SwiftUI view patterns and component guidance
- `swift-concurrency-expert` - Swift 6.2+ concurrency review and fixes
- `swiftui-liquid-glass` - iOS 26+ Liquid Glass API implementation
### OpenAI Codex
Place these skill folders under `$CODEX_HOME/skills/public` (or symlink this repo there).
### Optional
Enable the pre-commit hook to keep `docs/skills.json` in sync:
`git config core.hooksPath scripts/git-hooks`
## Skills