Skills に戻る
twostraws/swiftui-agent-skillチェック済み

SKILL DETAIL

swiftui-pro

twostraws/swiftui-agent-skill/swiftui-pro

Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects.

インストール · 280出典を見る

Installation

npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro

スキルファイル

SKILL.md

最終同期 · 2026/10/09

.claude-plugin/plugin.json›
{
  "name": "swiftui-pro",
  "version": "2.0.0",
  "description": "Reviews SwiftUI code for best practices on modern APIs, maintainability, and performance.",
  "author": { "name": "Paul Hudson" },
  "homepage": "https://github.com/twostraws/SwiftUI-Agent-Skill",
  "repository": "https://github.com/twostraws/SwiftUI-Agent-Skill",
  "license": "MIT",
  "skills": "./skills/"
}
agents/openai.yaml›
interface:
  display_name: "SwiftUI Pro"
  short_description: "Reviews SwiftUI code for modern best practices."
  icon_small: "./assets/swiftui-pro-icon.svg"
  icon_large: "./assets/swiftui-pro-icon.png"
  brand_color: "#006AFD"
  default_prompt: "Use $swiftui-pro to review my project."

policy:
  allow_implicit_invocation: true
assets/swiftui-pro-icon.svg›
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="227.72" height="227.72" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 227.72 227.72">
  <defs>
    <style>
      .st0 {
        fill: none;
        stroke: url(#linear-gradient1);
        stroke-miterlimit: 10;
        stroke-width: 6px;
      }

      .st1 {
        fill: url(#linear-gradient);
        fill-rule: evenodd;
      }
    </style>
    <linearGradient id="linear-gradient" x1="58.405" y1="206.955" x2="178.785" y2="86.575" gradientTransform="translate(0 260) scale(1 -1)" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#00d9ff"/>
      <stop offset="1" stop-color="#006efd"/>
    </linearGradient>
    <linearGradient id="linear-gradient1" x1="33.349" y1="218.651" x2="194.371" y2="57.629" gradientTransform="translate(0 252) scale(1 -1)" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#00eeaf"/>
      <stop offset="0" stop-color="#00d8ff"/>
      <stop offset="1" stop-color="#006ffd"/>
    </linearGradient>
  </defs>
  <path class="st1" d="M165.43,131.08c3.87-8.17,16.03-46.62-38.6-85.38,7.61,7.03,40.19,39.2,26.05,75.5-34.2-25.27-87.91-66.75-87.91-66.75,0,0,63.69,60.57,83.58,79.15,19.9,18.59,26.97,27.71,26.47,43.59,0,.01,14.58-23.23-9.59-46.11ZM130.89,145.79c-39.35,18.56-78.42-20.82-78.42-20.82,0,0,21.12,26.7,45.19,35.06,33.72,11.71,54.61-9.64,54.61-9.64L52.42,59.03s56.43,62.62,78.47,86.76Z"/>
  <circle class="st0" cx="113.86" cy="113.86" r="110.86"/>
</svg>
references/accessibility.md›
# Accessibility

- Respect the user's accessibility settings for fonts, colors, animations, and more.
- Do not force specific font sizes. Prefer Dynamic Type (`.font(.body)`, `.font(.headline)`, etc.).
- Read `@Environment(\.dynamicTypeSize)` for the user's text-size setting, rather than soft-deprecated `sizeCategory` and `ContentSizeCategory`. Its `DynamicTypeSize` value supports `isAccessibilitySize`, useful for selecting a layout that accommodates the largest text sizes.
- If you *need* a custom font size, use `@ScaledMetric` when targeting iOS 18 and earlier. When targeting iOS 26 or later, `.font(.body.scaled(by:))` is also available to get font size adjustment.
- Flag instances where images have unclear or unhelpful VoiceOver readings, e.g. `Image(.newBanner2026)`. If they are decorative, suggest using `Image(decorative:)` or `accessibilityHidden()`, otherwise attach an `accessibilityLabel()`.
- When `@Environment(\.accessibilityReduceMotion)` is true, replace large, motion-based animations with opacity changes instead.
- If buttons have complex or frequently changing labels, recommend using `accessibilityInputLabels()` to provide better Voice Control commands. For example, if a button had a live-updating share price for Apple such as "AAPL $271.68", adding an input label for "Apple" would be a big improvement.
- Buttons with image labels must always include text, even if the text is invisible: `Button("Label", systemImage: "plus", action: myAction)`. Flag icon-only buttons that lack a text label as being bad for VoiceOver. Usually SwiftUI will make labels use the correct label style based on their context – e.g. buttons in iOS toolbars will automatically be icon-only by default – but if there's a specific reason for a button to remain visually icon-only, apply `.labelStyle(.iconOnly)` to preserve the visual while keeping the text available for VoiceOver.
- Give `Menu` a text title as well as an icon: `Menu("Options", systemImage: "ellipsis.circle") { }` is much better than an image alone, because VoiceOver reads the title. In the rare case where the menu trigger should really display only the icon, apply `.labelStyle(.iconOnly)`.
- If color is an important differentiator in the user interface, make sure to respect the environment's `.accessibilityDifferentiateWithoutColor` setting by showing some kind of variation beyond just color – icons, patterns, strokes, etc.
- Never use `onTapGesture()` unless you specifically need tap location or tap count. All other tappable elements should be a `Button`.
- If `onTapGesture()` must be used, make sure to add `.accessibilityAddTraits(.isButton)` or similar so it can be read by VoiceOver correctly.
references/api.md›
# Using modern SwiftUI API

Many older SwiftUI declarations use `deprecated: 100000.0` in their SDK availability attributes. These are soft deprecations: Xcode stays quiet, so a warning-free build does not establish that the code uses current API. Review the source, and inspect a symbol's `@available` declaration in the SDK when its status is unclear.

Also check the Dynamic Type deprecations in `accessibility.md` and animatable modifier deprecations in `views.md`.

- Always use `foregroundStyle()` instead of `foregroundColor()`.
- Replace soft-deprecated `accentColor()` with `tint()`. Configure the asset catalog's accent color for an app-wide default.
- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`.
- Always use the `Tab` API instead of `tabItem()`.
- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none.
- For sizing and positioning, prefer `containerRelativeFrame()`, `visualEffect()`, or the `Layout` protocol to `GeometryReader` when they can do the job. To keep content clear of safe areas, rely on SwiftUI's default insets rather than measuring them. Pin real bar content with `safeAreaInset()` or `safeAreaBar()` (iOS 26+), which reserve space as well as placing it, and use `safeAreaPadding()` only for intentional extra spacing inside the safe area. All of these adapt when bars move, including iPhone Duo's vertical bar. Reading reserved regions on iOS 27.1+ is a valid exception: locating the fold or cameras requires a `GeometryProxy` supplied by `GeometryReader` or `onGeometryChange()`. Do not flag that usage.
- Use `toolbarVisibility()` in place of `toolbar(_:for:)` with a visibility or `navigationBarHidden()`, and `toolbarBackgroundVisibility()` in place of the visibility overload of `toolbarBackground(_:for:)`. The older names became soft-deprecated with the iOS 18 renames. For example, `.toolbarVisibility(.visible, for: .navigationBar)`. Keep `toolbarBackground()` when it supplies a background style.
- On iOS 27+, control status bar visibility through `toolbarVisibility(_:for:)` with `.statusBar`; `statusBarHidden()` is soft-deprecated there. Keep the older modifier for targets below iOS 27, where there is no replacement.
- Replace soft-deprecated `colorScheme()` with an environment override, such as `.environment(\.colorScheme, .light)`. Despite Apple's rename annotation, `preferredColorScheme()` is not equivalent: it affects the entire screen or sheet, whereas the environment override preserves the original view-and-descendants scope.
- Prefer `textInputAutocapitalization()` and `autocorrectionDisabled()` to the soft-deprecated `autocapitalization()` and `disableAutocorrection()`.
- Define a named coordinate space with `.coordinateSpace(.named("canvas"))`, replacing soft-deprecated `coordinateSpace(name:)`. Lookups still use `frame(in: .named("canvas"))`.
- Obtain color components with `color.resolve(in:)`, passing the relevant environment. It resolves red, green, blue, and opacity correctly for appearances such as light and dark mode without UIKit. Avoid soft-deprecated `Color.cgColor`, which returns `nil` for dynamic colors, including asset colors.
- With Xcode 27+, use `@ContentBuilder` for newly written view builders. SwiftUI now uses this name throughout most of its declarations; `@ViewBuilder` names the same type, so existing uses need no change and are not findings. This is a compiler/SDK requirement, with no deployment-target restriction.
- When designing haptic effects, prefer using `sensoryFeedback()` over older UIKit APIs such as `UIImpactFeedbackGenerator`.
- Use the `@Entry` macro to define custom `EnvironmentValues`, `FocusedValues`, `Transaction`, and `ContainerValues` keys. This replaces the legacy pattern of manually creating a type conforming to (for example) `EnvironmentKey` with a `defaultValue`, then extending `EnvironmentValues` with a computed property.
- An `@Entry` default must remain equal across reads. Constructing a reference instance such as `LibraryStore()`, a current date such as `.now`, or a fresh `UUID()` produces a new default each time, causing readers to update on unrelated environment changes. Prefer an optional entry without an explicit default, such as `@Entry var library: LibraryStore?`, or a shared `static let` fallback. Xcode 27 diagnoses reference-type defaults, but misses some changing value types; adding `Equatable` does not stabilize a changing value. Leave constant defaults such as `true`, `1`, or `.orange` alone.
- For custom environment or focused actions, pass a struct or `@Observable` instance containing the required data and a method, optionally `callAsFunction()`. A stored closure cannot be compared, so readers update even without a meaningful change; Xcode 27 warns about this. Putting that closure in a struct still has the same problem. System-provided actions, including `\.dismiss` and `\.openURL`, need no replacement.
- When `overlay()`, `background()`, or `mask()` receives a view, use its builder closure rather than its soft-deprecated view-argument overload: `.mask { Image(systemName: "star.fill") }`. Some argument forms also fail in Xcode 27 because `blendMode` becomes ambiguous; for example, write `.overlay { Color.orange.blendMode(.multiply) }`. Style overloads remain correct, including `.background(.indigo)` and `.background(.indigo, in: .rect(cornerRadius: 12))`.
- Never use deprecated `.navigationBarLeading` or `.navigationBarTrailing`. Prefer placements that express purpose: `.cancellationAction` for canceling, `.confirmationAction` for confirming, and `.primaryAction` for the screen's main action. Use `.topBarLeading` or `.topBarTrailing` when no semantic role fits. This lets each device position items correctly, particularly iPhone Duo's vertical bar, which sorts by role rather than leading or trailing side.
- Prefer to rely on automatic grammar agreement when dealing with English, French, German, Portuguese, Spanish, and Italian. For example, use `Text("^[\(people) person](inflect: true)")` to show a number of people.
- You can fill and stroke a shape with two chained modifiers; you do *not* need an overlay for the stroke. The overlay was required previously, but this is fixed in iOS 17 and later.
- When referencing images from an asset catalog, prefer the generated symbol asset API when the project is configured to use them: `Image(.avatar)` rather than `Image("avatar")`.
- When targeting iOS 26 and later, SwiftUI has a native `WebView` view type that replaces almost all uses of hand-wrapped `WKWebView` inside `UIViewRepresentable`. To use it, make sure to include `import WebKit`.
- iOS 26+: `ForEach` over an `enumerated()` sequence should not convert to an array first; use `ForEach(items.enumerated(), id: \.element.id)` directly. The `RandomAccessCollection` conformance that allows this is gated to iOS 26, so earlier targets keep `ForEach(Array(items.enumerated()), id: \.element.id)`.
- When hiding scroll indicators, use `.scrollIndicators(.hidden)` rather than `showsIndicators: false` in the initializer.
- Never concatenate `Text` values or construct a `Text` label by adding strings. For example, use `Text("Folder: \(folderName)")` instead of `Text("Folder: " + folderName)`: interpolation retains a localization key, while string addition produces a plain `String` whose literal prefix is not localized.

For example, the usage of `+` here is bad and deprecated:

```swift
Text("Hello").foregroundStyle(.red)
+
Text("World").foregroundStyle(.blue)
```

Instead, use text interpolation like this:

```swift
let red = Text("Hello").foregroundStyle(.red)
let blue = Text("World").foregroundStyle(.blue)
Text("\(red)\(blue)")
```


## Toolbars

These APIs require iOS 27 or later unless a different requirement is stated.

- Use `ToolbarOverflowMenu` for overflow actions. When toolbar items do not fit, the system now moves them into an overflow menu of its own; a custom ellipsis `Menu` then becomes a nested submenu, while `ToolbarOverflowMenu` contributes its actions directly. This applies on all devices, with especially little room on iPhone Duo where status, toolbar, and tabs occupy the same edge.
- A menu devoted to one task, such as filtering, can legitimately become a submenu, although it takes another tap to reach. Give it a descriptive SF Symbol rather than an ellipsis, and use `visibilityPriority(.high)` when it should remain in the bar.
- Assign `.high` visibility priority to frequent actions and items conveying status, and `.low` to expendable items. Without priorities, overflow starts at the trailing end without considering importance. Prioritization is particularly useful on iPhone Duo's outer display, which fits few items.
- Reserve `.topBarPinnedTrailing` for at most one indispensable action. It stays at the far trailing position, after the overflow control, and moves into the overflow menu only when search is active and room runs out; pinning multiple items undermines adaptive overflow.
- Consider `.toolbarMinimizationBehavior(.onScrollDown, for: .navigationBar)` for content-led screens such as articles or long lists. It shrinks a horizontal navigation bar during downward scrolling, along with an integrated top tab bar. iPhone Duo's vertical bar does not shrink. The iOS 26 tab bar equivalent is `.tabBarMinimizeBehavior(.onScrollDown)`.
- Apply `contentMarginsRemoved()` to the `ToolbarItem` holding custom imagery when the glass should fit the image. Default margins otherwise produce an oversized pill around an image button or a glass border around a standalone image. Keep standard margins on ordinary buttons.
- On any supported version, place related controls, such as previous and next actions, in a `ToolbarItemGroup` instead of separate items divided by fixed spacers. The group adapts its spacing and, where supported, accepts one visibility priority for the whole set.
- With Xcode 27+, a `ForEach` may generate items directly within `toolbar()`, replacing the need to spell out each item on older toolchains. This works with deployment targets back to iOS 16 without a runtime availability branch.
- Xcode 27.2+ and iOS 26.4+: toggle a toolbar item's `.hidden()` modifier instead of conditionally creating the item. For example, `ToolbarItem { Button("Export", systemImage: "square.and.arrow.up", action: export) }.hidden(!canExport)`. This rule is unavailable when building with Xcode 27.1; do not recommend it for that toolchain.


## Using ObservableObject

If using `ObservableObject` is absolutely required – for example if you are trying to create a debouncer using a Combine publisher – you should always make sure `import Combine` is added. This was previously provided through SwiftUI, but that is no longer the case.
references/data.md›
# Data flow, shared state, and property wrappers

It is important that SwiftUI body code and logic code be kept separate in order to make code easier to read, write, and maintain. That usually means placing code into methods rather than inline in the `body` property, but often also means carving functionality out into separate `@Observable` classes.

These rules help ensure code is efficient and works well in the long term.


## Shared state

- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Otherwise, flag any `@Observable` class missing this annotation.
- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing).
- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated.
- Where possible, give custom value types stored by an `@Observable` class `Equatable` conformance. Observation can then skip notifications for assignments equal to the current value. Structs and enums with associated values need explicit conformance; arrays are equatable only when their elements are.

Observation records the stored properties a view accesses. Reading a computed property also records every stored property used by its getter, so a getter does not narrow dependencies. A stored struct is one dependency: if `library.book` is a struct, reading `library.book.title` also subscribes to changes in `library.book.pageCount`. A computed `var bookTitle: String { book.title }` has the same dependency. Tracking just the title requires separating the fields into stored observable properties or making that nested model observable. These structural changes belong in a deep review or an investigation of excessive updates; see `performance-plus.md`.


## Local state

- Always declare view-owned `@State` as `private`. With Xcode 27, `@State` is a macro and retains its initial expression for one-time evaluation only for private properties. Otherwise an expression such as `SearchStore()` runs again whenever the parent reconstructs the view. Recommend correcting existing non-private state, but do not change it unasked: callers may currently supply that value through the initializer.
- Initialize each `@State` property in exactly one place. If `init()` supplies the value, leave the declaration without an initial expression and assign it once in the initializer. Providing both can produce a use-before-initialization diagnostic with Xcode 27; even where it compiles, Apple documents that the declaration's value wins.
- If a view stores a class instance that contains expensive-to-recompute data, e.g. `CIContext`, it can be stored using `@State` even though it is not an observable object. This effectively uses `@State` as a cache – storing something persistently, but not doing any change tracking on it since it's not an observable object.
- Keep window-specific UI state, such as the active tab, displayed record, or navigation path, in scene-root `@State` or `@SceneStorage`. In apps that support multiple scenes, `@AppStorage`, singletons, and shared observable models couple separate windows: iPad and iPhone Duo's inner display can host two app windows, which must be able to navigate independently.


## Bindings

- Prefer bindings projected from `@State`, `@Binding`, or `@Bindable`, including key-path bindings, over `Binding(get:set:)` in `body`. Closure bindings can add allocations and comparison problems that cause unnecessary updates. For conversions or lookups, expose a writable computed model property such as `$model.distanceInMiles`; for a parameterized lookup, use a labeled subscript such as `$model[quantityFor: product]`.
- Preserve behavior when refactoring bindings. A setter handles writes through that binding, including equal-value assignments; `onChange()` responds to observed value changes, including programmatic changes. Do not substitute one for the other without checking effects. Leave a custom binding intact when an equivalent replacement is unclear.
- If the user needs to enter a number into a `TextField`, bind the `TextField` to a numeric value such as `Int` or `Double`, then use its `format` initializer like this: `TextField("Enter your score", value: $score, format: .number)`. Apply either `.keyboardType(.numberPad)` (for integers) or `.keyboardType(.decimalPad)` (for floating-point numbers) as appropriate. Using the modifier alone is *not* sufficient.


## Working with data

- Prefer to make structs conform to `Identifiable` rather than using `id: \.someProperty` in SwiftUI code.
- Never attempt to use `@AppStorage` inside an `@Observable` class, even if marked `@ObservationIgnored` – it will *not* trigger view updates when a change happens.
- In mutable collections displayed by `ForEach`, `List`, or `Table`, identity must come from the element. Indices and offsets describe positions, so inserts, deletes, moves, and filtering can reset row state and disrupt animations. Avoid `ForEach(books.indices, id: \.self)` and `enumerated()` identified by `\.offset`. `ForEach(books.enumerated(), id: \.element.id)` is correct on iOS 26+, and `ForEach(Array(books.enumerated()), id: \.element.id)` below that; fixed ranges and constant collections are not a problem.
- Allocate an element's identity when storing the element, not while evaluating `body`. For example, mapping labels to fresh `Bookmark` values inside `ForEach` recreates every row if each initializer generates a UUID. A stored `let id = UUID()` is valid when its containing value survives body evaluations in `@State` or a model.
- Use a compact, immutable key to identify objects. Avoid `id: \.self` for a full struct, which hashes every field for every row on updates; reserve it for small unique values such as integers, UUIDs, URLs, or short strings. Do not derive identity from editable fields, such as `var id: String { caption }`: changing the caption then replaces the row and loses focus or selection.


## SwiftData

- If you only need the number of items matching a query, consider `ModelContext.fetchCount()` with a fetch descriptor. This will *not* live update if the data changes unless something else triggers the update, such as `@Query`, so it should be used carefully.

For more help with SwiftData, suggest the [SwiftData Pro agent skill](https://github.com/twostraws/swiftdata-agent-skill).

## If the project uses SwiftData with CloudKit

- Never use `@Attribute(.unique)`.
- Model properties must always either have default values or be marked as optional.
- All relationships must be marked optional.
references/design.md›
# Design

## Creating a uniform design in this app

Prefer to place standard fonts, sizes, colors, stack spacing, padding, rounding, animation timings, and more into a shared enum of constants, so they can be used by all views. This allows the app's design to feel uniform and consistent, and be adjusted easily.


## Requirements for flexible, accessible design

- Never rely on `UIScreen.main`. For available space, prefer `containerRelativeFrame()`, `visualEffect()`, or, when necessary, `GeometryReader`; for pixel scale, read `@Environment(\.displayScale)`. iPhone Duo has two displays, making a single main screen an invalid assumption, and the property is deprecated from iOS 26.
- Prefer flexible frames unless a fixed size can comfortably hold the content. Where an arrangement might run out of room, use `ViewThatFits` to attempt the preferred layout and then a smaller-space alternative, such as a horizontal control row followed by a vertical arrangement. Account for longer translations, larger Dynamic Type, and narrower windows.
- On iOS 26+, use `ConcentricRectangle()` for shapes next to a screen or sheet boundary, allowing their corners to follow the container. A fixed radius cannot match iPhone Duo's outer display, whose four corners differ. Shapes farther from these boundaries can continue using `.rect(cornerRadius:)`.
- Preserve access to the same features at every window size. Flag functionality gated to regular size classes or controls dropped from compact layouts: folding iPhone Duo must not make a feature disappear. Additional width can expose more of an existing hierarchy, such as list and detail together, without changing what the app can do.
- Apple's minimum acceptable tap area for interactions on iOS is 44x44. Ensure this is strictly enforced.


## Standard system styling

- Strongly prefer to use `ContentUnavailableView` when data is missing or empty, rather than designing something custom.
- When using `searchable()`, you can show empty results using `ContentUnavailableView.search` and it will include the search term they used automatically – there's no need to use `ContentUnavailableView.search(text: searchText)` or similar.
- If you need an icon and some text placed horizontally side by side, prefer `Label` over `HStack`.
- Prefer system hierarchical styles (e.g. secondary/tertiary) over manual opacity when possible, so the system can adapt to the correct context automatically.
- When using `Form`, wrap controls such as `Slider` in `LabeledContent` so the title and control are laid out correctly.
- `LabeledContent` also works outside `Form` for any title-value display; it might be necessary to define a custom `LabeledContentStyle` for consistent layout across views.
- When using `RoundedRectangle`, the default rounding style is `.continuous` – there is no need to specify it explicitly.


## Ensuring designs work for everyone

- Use `bold()` instead of `fontWeight(.bold)`, because using `bold()` allows the system to choose the correct weight for the current context.
- Only use `fontWeight()` for weights other than bold when there's an important reason - scattering around `fontWeight(.medium)` or `fontWeight(.semibold)` is counterproductive.
- Avoid hard-coded values for padding and stack spacing unless specifically requested.
- Avoid UIKit colors (`UIColor`) in SwiftUI code; use SwiftUI `Color` or asset catalog colors.
- The font size `.caption2` is extremely small, and is generally best avoided. Even the font size `.caption` is on the small side, and should be used carefully.
references/hygiene.md›
# Hygiene

- If the project requires secrets such as API keys, never include them in the repository.
- Code comments and documentation comments should be present where the logic isn't self-evident.
- Unit tests should exist for core application logic. UI tests only where unit tests are not possible.
- `@AppStorage` must never be used to store usernames, passwords, or other sensitive data. Use the keychain for that.
- If SwiftLint is configured, it should return no warnings or errors.
- Report a missing launch-screen declaration. Check for `UILaunchScreen` in Info.plist, `INFOPLIST_KEY_UILaunchScreen_Generation`, or an existing launch-screen storyboard configuration. App Store Connect rejects iOS 27 SDK builds without a launch screen. Leave project-setting changes to the developer rather than fixing this automatically.
- Flag `UIRequiresFullScreen` in Info.plist: with the iOS 27 SDK, it no longer prevents resizing, so code relying on it must adapt, including during iPhone Duo folds. Do not delete it or introduce `UIRequiresFullScreenIgnoredStartingWithVersion` without a request; either changes app behavior.
- If the Xcode MCP is configured, prefer its tools over generic alternatives. For example, `RenderPreview` is able to capture images of rendered SwiftUI previews for examination, and `DocumentationSearch` can search Apple's documentation for latest usage instructions.
references/localization.md›
# Localization

- Add text through the project's current localization resources. Existing `.strings` and `.stringsdict` workflows should continue; moving them to a String Catalog requires an explicit request.
- When the project uses `Localizable.xcstrings` and generated string symbols are enabled, add each new user-facing string as a symbol key with `extractionState` set to `"manual"`, then use the generated symbol, such as `Text(.libraryEmpty)`. Otherwise, follow the project's existing string-key access. Offer translations for the new keys in all the project's languages.
- For new properties, parameters, and enum payloads carrying localizable interface text, prefer `LocalizedStringResource`, for example `let explanation: LocalizedStringResource`. This applies equally to views, models, and other supporting types. `Text` displays a plain `String` verbatim; converting a runtime string to `LocalizedStringKey` does not make Xcode extract it into the catalog. Existing custom-view parameters typed as `LocalizedStringKey` are valid, and changing existing `String` storage belongs outside unrelated work.
- Before iOS 26, `LabeledContent` and the plain `TextField(_:text:)` do not accept a `LocalizedStringResource` title, so give them a `Text` label made from the resource. The other `TextField` forms, with `prompt:`, `axis:`, or `value:format:`, accept a resource from iOS 16, as do `SecureField`, `Text`, `Button`, `Label`, `Toggle`, `Section`, `Picker`, and `navigationTitle()`.
- Within a package or framework, specify the containing bundle for localized text: `Text("Reading list", bundle: #bundle)`. Prefer `#bundle` because it also works in frameworks, where the Swift package accessor `Bundle.module` is unavailable. Omitting the bundle argument makes SwiftUI search the application's main bundle, which can silently leave text untranslated.
- Read locale, calendar, and time zone through the view environment: `@Environment(\.locale)`, `@Environment(\.calendar)`, and `@Environment(\.timeZone)`. Their `.current` counterparts do not respect preview or per-view overrides.
- Add translator comments for context-sensitive wording, such as whether "Record" names an item or starts a recording. Explain every interpolated placeholder. For generated symbol keys, store this guidance in the String Catalog.
references/navigation.md›
# Navigation and presentation

- Use `NavigationStack` or `NavigationSplitView` as appropriate; flag all use of the deprecated `NavigationView`.
- Strongly prefer to use `navigationDestination(for:)` to specify destinations; flag all use of the old `NavigationLink(destination:)` pattern where it should be replaced.
- Never mix `navigationDestination(for:)` and `NavigationLink(destination:)` in the same navigation hierarchy; it causes significant problems.
- Register `navigationDestination(for:)` once per data type within each navigation stack; flag competing registrations in the same stack. Independent stacks, including those in separate tabs, can each register the same type.


## Alerts, confirmation dialogs, and sheets

- Attach `confirmationDialog()` to its triggering control so Liquid Glass animates from the right source. An action inside `ToolbarOverflowMenu` is the exception: the system owns that menu, so a dialog attached to a button inside it never appears, even though the button's action runs. Attach it to a view outside the menu and drive presentation with state.
- When building with Xcode 27 or later, use `alert(_:item:)` or `confirmationDialog(_:item:)` when presentation concerns one optional value, such as a pending removal. The optional supplies the unwrapped value to the closures and becomes `nil` on dismissal, without requiring `Identifiable`. This removes the separate Boolean and `presenting:` value, much as `sheet(item:)` does. These overloads are compiled into the app and support iOS 15 onward, or iOS 16 when the title is a `LocalizedStringResource`, without an availability check.
- If an alert has only a single "OK" button that does nothing but dismiss the alert, it can be omitted entirely: `.alert("Dismiss Me", isPresented: $isShowingAlert) { }`.
- If a sheet is designed to present an optional piece of data, prefer `sheet(item:)` over `sheet(isPresented:)` so the optional is safely unwrapped.
- When using `sheet(item:)` with a view that accepts the item as its only initializer parameter, prefer `sheet(item: $someItem, content: SomeView.init)` over `sheet(item: $someItem) { someItem in SomeView(item: someItem) }`.
references/performance-plus.md›
# Deeper performance work

These techniques change data and view structure to reduce update frequency, at the cost of more complicated code. Reserve them for an explicitly requested deep performance review, or an established problem such as excessive view updates that these changes can address.

Reading several properties from one `@Observable` object is not itself a reason to split a view. Observation already tracks those properties individually, and changes to unread properties do not update the view. Apply every recommendation below within that limit.

- When a view needs only a few fields of a struct, up to three, pass those fields individually. For example, `TrackTitle(title: track.title)` avoids updating for unrelated changes to `track`, unlike passing the entire value. If it needs four or more fields, consider passing the whole struct. This distinction does not apply to observable objects, whose properties are already tracked separately.
- Avoid passing a very large value through many view layers: nested collections or dozens of fields make whole-value comparisons expensive each time a parent updates. Supply a smaller value containing the particular view's inputs, or the existing observable object that owns the data. Do not change a struct into a class merely to avoid comparison costs.
- For a measured problem caused by a computed observable property, consider storing its result and refreshing it from `didSet` on each input property. Reading the cached property then creates one dependency instead of subscribing to everything the getter reads. Use this sparingly and maintain every update path; an incomplete cache produces stale UI.
- When one frequently changing field inside an observed struct invalidates unrelated readers, move that field into a separate stored property on the observable object. Otherwise a view reading `player.track.title` also updates when `player.track.playCount` changes, because `track` is a single dependency.
- If many independent views consume the same small portion of a large observable model and still update excessively, extract that portion into a smaller `@Observable` object and pass it directly. Those views then subscribe to the smaller model alone. Do not make this change merely because the parent model has many properties.
- Where row fields change independently, a separate `@Observable` instance per element can confine notifications to rows reading the changed fields. Create these instances once and retain them in the parent; avoid wholesale conversion of existing value models.
- If an expensive body reads a property solely for `onChange()`, isolate both the observation and modifier in a small `ViewModifier`. Otherwise every change reevaluates the expensive body for data it never displays. This is worthwhile only when that read has no other purpose in `body` and the cost is established.
- For per-row results derived from scrolling, such as visibility, store each result in that row's own observable model. A row then updates when its derived value changes, rather than on every movement of the scroll position.
references/performance.md›
# Performance

- Keep conditional modifier values inside the modifier, for example `.opacity(isUnavailable ? 0.4 : 1)`, rather than branching the view with `if`/`else`. Branching produces `_ConditionalContent`, which changes structural identity and recreates the underlying platform views each time the condition flips. Never introduce a `View.if()` helper that conditionally transforms its receiver: it changes structural identity, resets state, and disrupts animations when the condition flips. Report existing helpers with this explanation, but leave their removal to a requested change because they may have many callers.
- If a style ternary fails because its branches have different types, erase each style with `AnyShapeStyle`, for example `.background(isHighlighted ? AnyShapeStyle(.thinMaterial) : AnyShapeStyle(.clear))`. Keep the same view instead of introducing branches. Unlike `AnyView`, `AnyShapeStyle` is not a performance concern; use it only when ordinary type inference cannot handle the ternary.
- Avoid `AnyView` unless absolutely required. Use generics, `Group`, an extracted `View` struct, or a `@ContentBuilder` closure parameter instead.
- If a `ScrollView` has an opaque, static, and solid background, prefer to use `scrollContentBackground(.visible)` to improve scroll-edge rendering efficiency.
- For view extraction, follow the guidance and exceptions in `views.md`.
- Always ensure view initializers are kept as small and simple as possible, avoiding any non-trivial work. Flag any work that can be moved into a `task()` modifier to be run when the view is shown.
- Similarly, assume each view's `body` property is called frequently – if logic such as sorting or filtering can be moved out of there easily, it should be.
- Avoid creating properties to store formatters such as `DateFormatter` unless they are required. A more natural approach is to use `Text` with a format, like this: `Text(Date.now, format: .dateTime.day().month().year())` or `Text(100, format: .currency(code: "USD"))`.
- Avoid expensive inline transforms in `List`/`ForEach` initializers (e.g. `items.filter { ... }`) when they are repeated often.
- Prefer deriving transformed data from the source-of-truth using `let`, or caching in `@State`. However, do not cache derived collections in `@State` unless you also own explicit invalidation logic to avoid stale UI.
- For large data sets in `ScrollView`, use `LazyVStack`/`LazyHStack`; flag eager stacks with many children.
- Prefer using `task()` over `onAppear()` when doing async work, because it will be cancelled automatically when the view disappears.
- Avoid storing escaping `@ContentBuilder` closures on views when possible; store built view results instead.

`@ContentBuilder` requires Xcode 27+ but no minimum iOS. Use `@ViewBuilder` with earlier toolchains; existing uses of that name are not performance findings.

Example:

```swift
// Anti-pattern: stores an escaping closure on the view.
struct CardView<Content: View>: View {
    let content: () -> Content

    var body: some View {
        VStack(alignment: .leading) {
            content()
        }
        .padding()
        .background(.ultraThinMaterial)
        .clipShape(.rect(cornerRadius: 8))
    }
}

// Preferred: store the built view value; the synthesized init handles calling the builder.
struct CardView<Content: View>: View {
    @ContentBuilder let content: Content

    var body: some View {
        VStack(alignment: .leading) {
            content
        }
        .padding()
        .background(.ultraThinMaterial)
        .clipShape(.rect(cornerRadius: 8))
    }
}
```


## Collections and row updates

- Pass each row its element, such as `BookmarkRow(bookmark: bookmark)`. Passing a store and a position or identifier makes the row search the collection, creating a dependency on the entire collection so one element's changes can update all rows.
- If rows own separate observable models, create and retain those models once. Constructing `BookmarkRow(model: BookmarkModel(bookmark: bookmark))` inside `body` or a `ForEach` callback gives every row a different object on each update.
- In a `List` or other lazy container, make each `ForEach` element produce the same number and kind of top-level views every time: normally one view, or a deliberate fixed group, never a count or shape that depends on the element. A bare `if`, `if`/`else`, or `switch` at the top of a row breaks this even when every branch yields one view, because which branch ran becomes part of the row's identity. When the count and kind are constant, SwiftUI derives every row's identity from the element ids alone; when they are not, it runs every row's body on each update to find out, a cost that grows with the list's length. Wrap such content in a container such as `VStack`; `Group` does not count. To leave elements out, filter the collection before `ForEach` instead of using an `if` inside it. The collection can be any size and change freely; the constraint is on what each element produces. A ternary remains right when only modifier values differ.
- To investigate variable row counts in lazy containers, try the launch argument `-LogForEachSlowPath YES`. This logs the affected `ForEach` instances.


## Frequent changes

- Keep rapidly changing measurements, such as scroll positions, geometry, drag coordinates, animation progress, and timer ticks, out of the environment. Each environment write makes SwiftUI inspect the descendant tree, even where no view reads that value.
- Store those measurements in an `@Observable` object and expose separately stored, less frequently changing results, such as `hasReachedEnd`, for views to read. Converting the raw measurement to a Boolean inside `body` still subscribes that view to every raw update.
- Use `scrollTransition()` or `visualEffect()` for scroll-driven appearance changes such as fading, scaling, and rotation; they avoid reevaluating `body`. Keep state when scroll position also drives application logic.
- Remove unread key-path `@Environment` and `@FocusedValue` properties. Their declarations subscribe the view even without a read in `body`. An unused type-based declaration such as `@Environment(LibraryStore.self)` has no runtime overhead, but is still unnecessary code.


## Remote images

- On iOS 27, `AsyncImage` honors HTTP caching headers automatically, including in apps with older deployment targets. Do not introduce an image cache or third-party dependency solely to cache those requests. For additional control with an iOS 27+ target, use `AsyncImage(request:)` with a `URLRequest` cache policy, or supply a session through `asyncImageURLSession()`.
references/resizability.md›
# Resizable windows and adaptive layouts

Treat window dimensions as changeable throughout a scene's lifetime. iPad supports freely resized windows; iPhone Duo, Apple's folding iPhone, changes the app's available space as it opens and closes. Its outer display is broader and considerably shorter than other iPhones, while its larger inner display has a central fold. Most guidance below applies across devices. Rules marked iOS 27.1+ require that deployment target or an `if #available(iOS 27.1, *)` branch.

Do not introduce newer layout APIs solely to modernize working code. Recommend additions only for requested behavior or a concrete layout problem; apply them within the user's requested scope. Keep deployment targets unchanged, and preserve an older-system fallback when adding an availability branch.

Strongly recommend Xcode 27 or newer, preferably 27.1. Builds made with Xcode 26 run on iPhone Duo in a conventional iPhone-shaped compatibility area; Xcode 27.0 expands inner-display use, and Xcode 27.1 enables the full display and vertical bars.


## Choosing layouts

- Base layout choices on `@Environment(\.horizontalSizeClass)` and `@Environment(\.verticalSizeClass)`, not device idiom or orientation. iPhone Duo identifies as a phone but changes from compact outer-display width to regular width and height inside; neither `UIDevice.current.userInterfaceIdiom` nor rotation describes the available layout space.
- Test for `== .regular` when choosing an expanded layout. Size classes are optional, so `!= .compact` incorrectly treats `nil` as spacious.
- Flag forced size-class overrides, such as `.environment(\.verticalSizeClass, .regular)`, for review. They prevent descendants from responding to real window changes; Apple warns that they are likely to interfere with adaptation, rather than forbidding every use.
- A portrait-only orientation policy does not guarantee a narrow window. iPhone Duo's inner display supplies regular width and height regardless of the app's supported rotations.
- Give screens that assume everything fits, such as account setup, purchase screens, and introductions, scrolling or a `ViewThatFits` fallback. iPhone Duo's outer display is wide and short, nearer a square than other iPhones, so a layout sized for a tall phone can push its bottom actions out of reach.
- Folding resizes the current scene without relaunching the app or changing `scenePhase`; it needs no dedicated save/restore flow. When size classes select different view hierarchies, keep selections and navigation paths above the branch. State inside a replaced hierarchy loses its view identity and resets.
- With two windows open, closing the device keeps the most recently used window active and backgrounds the other. Existing background-save logic should expect that transition.


## Safe areas

- Remove padding that imitates a system safe-area inset, such as a fixed bottom gap intended to clear the home indicator. SwiftUI already respects that boundary, and its size varies. Do not substitute `safeAreaPadding()` for the guessed inset: that adds a design margin, which is appropriate only when additional spacing is intentional.
- When interactive or readable content must extend beyond a safe area, specify the intended edges, for example `.ignoresSafeArea(edges: .top)`. Unqualified `ignoresSafeArea()` is appropriate for decoration such as backgrounds, gradients, images, and scrims; on content it can put text and controls beneath system bars, including vertical ones.
- On iOS 26+, attach custom control bars with `safeAreaBar()` instead of layering them in a `ZStack` or overlay and guessing clearance with padding. It reserves space and supplies the system scroll-edge treatment for readable content underneath. Use `safeAreaInset()` for other pinned content, such as notices, or as the bar fallback on earlier targets. Pure decoration needs neither modifier.
- On iOS 26+, extend imagery behind sidebars, inspectors, or vertical bars with `backgroundExtensionEffect()`. Its mirrored, blurred extension preserves the visible original image, whereas `ignoresSafeArea()` can conceal part of it behind the bar. Apple recommends using one extension per screen.


## Tab bars and toolbars

- When creating tabs, navigation, and toolbars, prefer `TabView` and system toolbars to get the best system-native experience. iPhone Duo can combine them into a vertical outer-display bar; custom bars do not automatically relocate, overflow, or clear cameras, and no API adds those behaviors. If a custom bar is necessary, position it using `toolbarVerticalEdge` and reserved-region geometry on supported systems.
- Supply both text and an icon for each tab and toolbar action, such as `Button("Compose", systemImage: "square.and.pencil", action: compose)`. Vertical bars show icons, while overflow menus also show titles. Title-only or custom-view items don't join the vertical bar.
- Follow the semantic toolbar placement rules in `api.md`. Vertical bars arrange actions by purpose, not by a horizontal leading/trailing position.
- In `NavigationSplitView`, attach toolbar declarations to the column whose content they control. Only detail-column actions migrate to the vertical bar; sidebar and content-column actions remain above their respective columns.
- iOS 27.1+, iPhone Duo: read `@Environment(\.toolbarVerticalEdge)` when aligning custom UI to the bar. Its side follows device orientation and does not mirror with right-to-left languages, so a fixed edge is unreliable. The value is `nil` in contexts that never use a vertical bar.
- iOS 27.1+, iPhone Duo: use `axisBehavior(.horizontalOnly)` sparingly to keep an item horizontal; retaining a top bar consumes content height. `.verticalPreferred` instead favors the vertical bar when both bars exist.
- iOS 27.1+: report `toolbarVerticalBehavior(.disabled)` except where horizontal bars genuinely serve better: a sheet with only a Done/Close action, a full-width interface concentrated at the bottom, such as a dialing pad or calculator, or a full-screen video player with toolbar controls. Treat the choice as fixed rather than toggled at runtime. Preserve the default vertical adaptation elsewhere, and never propose adding this opt-out.
- Only recommend `toolbarVerticalCompressionBehavior(.prefersToolbarItems)` when a requested iPhone Duo layout should prioritize actions over tabs, or testing identifies a problem with action visibility. It requires an iOS 27.1+ target or a guarded branch with an older-system fallback. Tabs normally take precedence in the shared vertical bar, pushing actions into overflow; this modifier preserves the actions and condenses tabs into one control, which can suit focused workflows such as composing a document.
- iOS 27+: to turn tabs into a sidebar on wide iPhone layouts, including iPhone Duo's inner display, combine `.tabViewStyle(.sidebarAdaptable)` with `.defaultTabBarPlacement(.sidebar)`. This conversion is not automatic on iPhone.


## Presentations

- Prefer system sheets with `presentationDetents()`, popovers, and alerts. They adjust around the fold; a custom sheet assembled from a `ZStack`, offsets, and a drag gesture does not gain that behavior.
- Only recommend `.presentationPlacement(.trailing)` or `.leading` when side placement is requested or a sheet obscures content that needs to remain visible. It requires an iOS 27+ target or a guarded branch retaining the existing presentation on older systems. This suits wide iPad windows and iPhone Duo's inner display, where sheets otherwise appear centrally. The modifier applies only to sheets.
- Use `inspector(isPresented:content:)` for an adjacent settings or detail panel. It becomes a trailing column at regular width and a sheet at compact width, replacing separate hand-built panel and sheet hierarchies.


## Fold and camera geometry

These APIs require iOS 27.1+.

- Add reserved-region handling only when a requested full-screen custom layout must avoid the fold or cameras, or testing reveals an overlap. System containers already account for these regions. Require an iOS 27.1+ target or a guarded branch preserving the older layout. Read `reservedRegions(kind: .division)` for the fold or `reservedRegions(kind: .occlusion)` for cameras from a `GeometryProxy`, obtained through `GeometryReader` or `onGeometryChange()`. Use each region's `frame` and check `isActive`; the fold becomes relevant when partly folded. Do not hardcode dimensions or positions. Frames already account for right-to-left mirroring.
- Keep controls clear of active camera regions, where touches are suppressed. The outer camera always reserves space; the inner camera occludes content only while being used.
- Consider `ArrangementView` only for a requested adaptive primary/secondary layout or a demonstrated resizing or fold problem with an existing pair, such as a document with notes or a video with a transcript. Require an iOS 27.1+ target or a guarded branch preserving the older layout. `.arrangementViewStyle(.split)` chooses side-by-side or stacked placement; `.split.axes(.horizontal)` limits the allowed direction. `.arrangementViewStyle(.overlay)` layers the primary above the secondary, then separates them across a partial fold. This works on all iPhones and iPads running iOS 27.1, but is not a general replacement for stacks.
- Keep `NavigationStack`, `NavigationSplitView`, and `TabView` outside the `ArrangementView`, wrapping it rather than appearing within its two children. Never nest an `ArrangementView` in a `ScrollView` or `List`.
- Ensure essential secondary content has another access route. When the allowed split cannot fit, only the primary remains, as can happen on the outer display in landscape. `layoutPriority()` determines which content survives when space is insufficient.
- Inside an arrangement, read `@Environment(\.splitArrangementAxis)` to adapt to the split direction. In an overlay arrangement, the primary's `@Environment(\.overlayArrangementZIndex)` is 1 while above the secondary and 0 after separation around the fold. Neither environment value indicates whether the other child is hidden.
- Reserve `onHingeChange()` for effects or interactions tied to hinge movement; it reports angle and status. Layout decisions belong to size classes, reserved regions, and `ArrangementView`. Angles range from 180 (fully open) to 0 (fully closed), currently with individual degree accuracy. This is system policy, and may change in the future.
- Do not plan simultaneous app content on both displays. The outer display cannot host the app's content while the device is open, except for a camera app during capture.


## Checking changes

For iPhone Duo work, inspect every screen, sheet, and popover on both displays in all rotations. Include vertical bars, controls near a partial fold, Split View on either side, and a right-to-left language. Folding and rotation in the simulator require manual Device Hub interaction. Check the outer-display cases available to you, then ask the user to cover the remaining manual cases and state which were not tested.
references/swift.md›
# Swift

- Prefer Swift-native string methods over Foundation equivalents: use `replacing("a", with: "b")` not `replacingOccurrences(of: "a", with: "b")`.
- Prefer modern Foundation API: `URL.documentsDirectory` instead of `FileManager` directory lookups, `appending(path:)` to append strings to a URL.
- Never use C-style number formatting like `String(format: "%.2f", value)`. Use `Text(value, format: .number.precision(.fractionLength(2)))` or similar `FormatStyle` APIs.
- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`.
- Avoid force unwraps (`!`) and force `try` unless the failure is truly unrecoverable, and even then prefer using `fatalError()` with a clear description. If possible, use `if let`, `guard let`, nil-coalescing, or `try?`/`do-catch`.
- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()` or `localizedCaseInsensitiveContains()`.
- Strongly prefer `Double` over `CGFloat`, except where an API requires `CGFloat`, including optionals, `inout`, and generic containers such as `[CGFloat]`.
- If you want to count array objects that match a predicate, always use `count(where:)` rather than `filter()` followed by `count`.
- Prefer `Date.now` over `Date()` for clarity.
- When `import SwiftUI` is already in a file, you do not need to add `import UIKit` or `import AppKit` to access things like `UIImage` or `NSImage` – they are imported automatically on the appropriate platform.
- When dealing with the names of people, strongly prefer to use `PersonNameComponents` with modern formatting over simple string interpolation such as `Text("\(firstName) \(lastName)")`.
- If a given type of data is repeatedly sorted using an identical closure, e.g. `books.sorted { $0.author < $1.author }`, prefer to make the type in question conform to `Comparable` so the sort order is centralized.
- Prefer modern date formatting over handwritten patterns. If user-visible dates require `DateFormatter`, configure it with `setLocalizedDateFormatFromTemplate()` rather than assigning `dateFormat`, so the locale determines component ordering. Use `y`, rather than `yyyy`, for years in the template. Fixed formats used for API exchange are outside this rule.
- Format lists for display with `formatted()` rather than inserting comma separators yourself. For example, `destinations.formatted()` provides the locale's punctuation and conjunction; `.formatted(.list(type: .or))` uses the alternative conjunction, and `.list(memberStyle:type:)` formats non-string members. Keep `joined(separator:)` for storage or interchange where a fixed delimiter is intentional.
- Outside SwiftUI, create localized strings with `String(localized:)` rather than `NSLocalizedString()`. Xcode cannot extract interpolated keys from the older function, leaving that text untranslated.
- When trying to convert a string to a date, prefer the modern `Date` initializer API such as `Date(myString, strategy: .iso8601)`.
- Flag instances where errors triggered by a user action are swallowed silently, e.g. using `print(error.localizedDescription)` rather than showing an alert or similar.
- Prefer `if let value {` shorthand over `if let value = value {`.
- Omit `return` for single expression functions. `if` and `switch` can be used as expressions when returning values and assigning to variables.

For example, this kind of code:

```swift
var tileColor: Color {
    if isCorrect {
        return .green
    } else {
        return .red
    }
}
```

Should be written like this:

```swift
var tileColor: Color {
    if isCorrect {
        .green
    } else {
        .red
    }
}
```


## Swift Concurrency

- If an API offers both modern `async`/`await` equivalents and older closure-based variants, prefer the `async`/`await` versions.
- Never use Grand Central Dispatch (`DispatchQueue.main.async()`, `DispatchQueue.global()`, etc.). Always use modern Swift concurrency (`async`/`await`, actors, `Task`).
- Never use `Task.sleep(nanoseconds:)`; use `Task.sleep(for:)` instead.
- Flag any mutable shared state that isn't protected by an actor or `@MainActor`, unless the project is configured to use MainActor default actor isolation.
- Assume strict concurrency rules are being applied; flag `@Sendable` violations and data races.
- When evaluating `MainActor.run()`, check whether the project has its default actor isolation set to Main Actor first, because `MainActor.run()` might not be needed.
- `Task.detached()` is often a bad idea. Wherever it is used, check that it is the best approach.

For more help with Swift concurrency, suggest the [Swift Concurrency Pro agent skill](https://github.com/twostraws/swift-concurrency-agent-skill).
references/views.md›
# SwiftUI Views

- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ContentBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file.
- Flag `body` properties that are excessively long; they should be broken into extracted subviews.
- If the user has created a handful of small, private helper `some View` properties for structural readability, and they both belong to the same concern as `body` and would fit in `body` at an acceptable length if inlined, these can be left alone. Otherwise, they should be extracted to new `View` structs.
- Button actions should be extracted from view bodies into separate methods, to avoid mixing layout and logic.
- Similarly, general business logic should not live inline in `task()`, `onAppear()` or elsewhere in `body`.
- Prefer to place view logic into view models or similar, so it can be tested. For more help with testing, suggest the [Swift Testing Pro agent skill](https://github.com/twostraws/swift-testing-agent-skill).
- Each type (struct, class, enum) should be in its own Swift file. Flag files containing multiple type definitions.
- Unless a full-screen editing experience is required, prefer using `TextField` with `axis: .vertical` to using `TextEditor`, because it allows placeholder text. If a specific minimum height is required for `TextField`, use something like `lineLimit(5...)`.
- If a button action can be provided directly as an `action` parameter, do so. For example: `Button("Label", systemImage: "plus", action: myAction)` is preferred over `Button("Label", systemImage: "plus") { action() }`.
- When rendering SwiftUI views to images, strongly prefer `ImageRenderer` over `UIGraphicsImageRenderer`.
- `#Preview` should be used for previews, not the legacy `PreviewProvider` protocol.
- When using `TabView(selection:)`, use a binding to a property that stores an enum rather than an integer or string. For example, `Tab("Home", systemImage: "house", value: .home)` is better than `Tab("Home", systemImage: "house", value: 0)`.
- Strongly prefer to avoid breaking up view bodies using computed properties or methods that return `some View`, even if `@ContentBuilder` is used. Extract them into separate `View` structs instead, placing each into its own file. (Yes, this is repeated, but it's so important it needs to be mentioned twice.)
- Remove a `Group` that merely wraps one view; attach its modifiers to that view directly. For example, `Group { Image(systemName: "leaf") }` adds no visual behavior, but gives the compiler another type to check throughout the modifier chain. Groups containing siblings, a `ForEach`, or conditional branches serve a purpose and should stay, except around branches inside a `List` row, where a `VStack` is needed instead so that the row's shape does not depend on which branch ran.

Examples use the Xcode 27+ name `@ContentBuilder`, which has no minimum iOS; keep `@ViewBuilder` on earlier toolchains and leave existing uses unchanged.


## Animating views

- Strongly prefer the `@Animatable` macro: it supplies `Animatable` conformance and `animatableData` automatically. Mark properties that cannot or should not animate, such as integers and Booleans, with `@AnimatableIgnored`. Write `animatableData` manually only when its setter needs behavior the macro cannot generate, such as range limiting or normalization; use `AnimatableValues` on iOS 26+, or `AnimatablePair` for earlier targets.
- For an animated `ViewModifier`, use `@Animatable` rather than the deprecated `AnimatableModifier` protocol.
- Never use `animation(_ animation: Animation?)`; always provide a value to watch, such as `.animation(.bouncy, value: score)`.
- Chaining animations must be done using a `completion` closure passed to `withAnimation()`, rather than trying to execute multiple `withAnimation()` calls using delays.

For example:

```swift
Button("Animate Me") {
    withAnimation {
        scale = 2
    } completion: {
        withAnimation {
            scale = 1
        }
    }
}
```


## Reordering and swipe actions

The following requires iOS 27+ and is unavailable on tvOS.

- Enable drag reordering with `reorderable()` on the `ForEach` and `reorderContainer(for:)` on its enclosing container. Apply the callback's `ReorderDifference` to the source data yourself; SwiftUI does not mutate it. This works in lists, lazy stacks and grids, ordinary stacks, and custom layouts. Earlier versions require `List` with `onMove()` or a custom drag implementation. Elements must conform to `Identifiable`, or use the `itemID:` overload.

For example:

```swift
ScrollView {
    LazyVStack {
        ForEach(bookmarks) { bookmark in
            BookmarkRow(bookmark: bookmark)
        }
        .reorderable()
    }
    .reorderContainer(for: Bookmark.self) { change in
        change.apply(to: &bookmarks)
    }
}
```

SwiftUI has no built-in way to apply this difference. Add a helper like this one, adapted from an Apple example, in its own file:

```swift
extension ReorderDifference where CollectionID == ReorderableSingleCollectionIdentifier {
    func apply<Items>(to items: inout Items)
    where Items: RangeReplaceableCollection,
          Items.Element: Identifiable,
          Items.Element.ID == ItemID {
        let sourceIDs = Set(sources)
        var relocated: [Items.Element] = []

        items.removeAll { candidate in
            if sourceIDs.contains(candidate.id) {
                relocated.append(candidate)
                return true
            }
            return false
        }

        switch destination.position {
        case .before(let followingID):
            let insertionPoint = items.firstIndex { $0.id == followingID } ?? items.endIndex
            items.insert(contentsOf: relocated, at: insertionPoint)
        case .end:
            items.append(contentsOf: relocated)
        }
    }
}
```

- For moves across sections, identify each inner collection with `reorderable(collectionID:)`, and use `reorderContainer(for:in:)` on the enclosing container. Read `difference.destination.collectionID` to determine the receiving section.
- Reordering needs no drag-and-drop code; `reorderContainer(for:)` already acts as the drag container. Add `dragContainer(for:)` to the same container only when items should also take part in system drag and drop, such as dragging out to other views or apps, and make the item type `Transferable`. A `draggable()` on a row has no effect on reordering.
- Swipe actions also work in scroll views containing lazy stacks, lazy grids, or ordinary stacks. Apply `swipeActions()` to the rows and `swipeActionsContainer()` to the `ScrollView`; omitting the latter leaves the actions inactive without an error.
SKILL.md›
---
name: swiftui-pro
description: Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects.
license: MIT
metadata:
  author: Paul Hudson
  version: "2.0.0"
---

Review Swift and SwiftUI code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems – do not nitpick or invent issues.

Include deprecated API in review findings. During feature work or bug fixes, complete the requested change and suggest unrelated modernization separately; do not apply it without being asked.

Review process:

1. Check for deprecated API using `references/api.md`.
1. Check that views, modifiers, and animations have been written optimally using `references/views.md`.
1. Validate that data flow is configured correctly using `references/data.md`.
1. Ensure navigation is updated and performant using `references/navigation.md`.
1. Ensure the code uses designs that are accessible and compliant with Apple's Human Interface Guidelines using `references/design.md`.
1. Review layout behavior as windows resize, including iPad multitasking and folding iPhone Duo, using `references/resizability.md`.
1. Validate accessibility compliance including Dynamic Type, VoiceOver, and Reduce Motion using `references/accessibility.md`.
1. Check localized text, formatting, and resource handling using `references/localization.md`.
1. Ensure the code is able to run efficiently using `references/performance.md`.
1. Quick validation of Swift code using `references/swift.md`.
1. Final code hygiene check using `references/hygiene.md`.

If doing a partial review, load only the relevant reference files.

Keep `references/performance-plus.md` out of routine reviews. Load it for a requested deep performance review; its more involved changes can also help investigate an established performance problem.


## Core Instructions

- iOS 27 exists, and is the default deployment target for new apps.
- Xcode 27.1 and the corresponding 27.1 platform releases are available. Do not dismiss their APIs as speculative.
- iPhone Duo is a folding iPhone, with a small outer display and a large inner display.
- Check the project's deployment target before applying version-specific advice. Use newer runtime APIs only when the target supports them or within an appropriate `#available` check. Keep the existing target unchanged. Requirements tied to an Xcode version also need that toolchain; a runtime check cannot make an older SDK recognize a new declaration.
- Target Swift 6.4 or later, using modern Swift concurrency.
- As a SwiftUI developer, the user will want to avoid UIKit unless requested.
- Do not introduce third-party frameworks without asking first.
- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file.
- Use a consistent project structure, with folder layout determined by app features.


## Output Format

Organize findings by file. For each issue:

1. State the file and relevant line(s).
2. Name the rule being violated (e.g., "Use `foregroundStyle()` instead of `foregroundColor()`").
3. Show a brief before/after code fix.

Skip files with no issues. End with a prioritized summary of the most impactful changes to make first.

Example output:

### ContentView.swift

**Line 12: Use `foregroundStyle()` instead of `foregroundColor()`.**

```swift
// Before
Text("Hello").foregroundColor(.red)

// After
Text("Hello").foregroundStyle(.red)
```

**Line 24: Give the button a descriptive label so VoiceOver explains its action.**

```swift
// Before
Button(action: addUser) {
    Image(systemName: "plus")
}

// After
Button("Add User", systemImage: "plus", action: addUser)
```

**Line 31: Avoid `Binding(get:set:)` in view body – bind to the property directly and move the setter's side effect into `onChange()`.**

```swift
// Before
TextField("Username", text: Binding(
    get: { model.username },
    set: { model.username = $0; model.save() }
))

// After
TextField("Username", text: $model.username)
    .onChange(of: model.username) {
        model.save()
    }
```

Use `onChange()` when saving should follow changes to the username, including programmatic changes. Equal-value assignments will not trigger a save.

### Summary

1. **Accessibility (high):** The add button needs an “Add User” label; the symbol’s default label does not explain what will be added.
2. **Deprecated API (medium):** `foregroundColor()` on line 12 should be `foregroundStyle()`.
3. **Data flow (medium):** The manual binding on line 31 is fragile and harder to maintain.

End of example.


## References

- `references/accessibility.md` - Dynamic Type, VoiceOver, Reduce Motion, and other accessibility requirements.
- `references/api.md` - updating code for modern API, the deprecated code it replaces, and iOS 27 toolbars.
- `references/design.md` - guidance for building accessible apps that meet Apple's Human Interface Guidelines.
- `references/hygiene.md` - making code compile cleanly and be maintainable in the long term.
- `references/localization.md` - localizable text, resource bundles, and locale-aware presentation.
- `references/navigation.md` - navigation using `NavigationStack`/`NavigationSplitView`, plus alerts, confirmation dialogs, and sheets.
- `references/performance.md` - optimizing SwiftUI code for maximum performance.
- `references/performance-plus.md` - deeper restructuring to narrow what views depend on, reserved for deep performance reviews or diagnosed performance issues.
- `references/resizability.md` - changing window sizes, adaptive bars, safe areas, and iPhone Duo layouts.
- `references/data.md` - data flow, shared state, and property wrappers.
- `references/swift.md` - tips on writing modern Swift code, including using Swift Concurrency effectively.
- `references/views.md` - view structure, composition, animation, reordering, and swipe actions.
skills/swiftui-pro/SKILL.md›
---
name: swiftui-pro
description: Comprehensively reviews SwiftUI code for best practices on modern APIs, maintainability, and performance. Use when reading, writing, or reviewing SwiftUI projects.
license: MIT
argument-hint: "[focus area]"
metadata:
  author: Paul Hudson
  version: "2.0.0"
---

Review Swift and SwiftUI code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems – do not nitpick or invent issues.

Include deprecated API in review findings. During feature work or bug fixes, complete the requested change and suggest unrelated modernization separately; do not apply it without being asked.

Review process:

1. Check for deprecated API using `${CLAUDE_SKILL_DIR}/references/api.md`.
1. Check that views, modifiers, and animations have been written optimally using `${CLAUDE_SKILL_DIR}/references/views.md`.
1. Validate that data flow is configured correctly using `${CLAUDE_SKILL_DIR}/references/data.md`.
1. Ensure navigation is updated and performant using `${CLAUDE_SKILL_DIR}/references/navigation.md`.
1. Ensure the code uses designs that are accessible and compliant with Apple's Human Interface Guidelines using `${CLAUDE_SKILL_DIR}/references/design.md`.
1. Review layout behavior as windows resize, including iPad multitasking and folding iPhone Duo, using `${CLAUDE_SKILL_DIR}/references/resizability.md`.
1. Validate accessibility compliance including Dynamic Type, VoiceOver, and Reduce Motion using `${CLAUDE_SKILL_DIR}/references/accessibility.md`.
1. Check localized text, formatting, and resource handling using `${CLAUDE_SKILL_DIR}/references/localization.md`.
1. Ensure the code is able to run efficiently using `${CLAUDE_SKILL_DIR}/references/performance.md`.
1. Quick validation of Swift code using `${CLAUDE_SKILL_DIR}/references/swift.md`.
1. Final code hygiene check using `${CLAUDE_SKILL_DIR}/references/hygiene.md`.

If doing a partial review, load only the relevant reference files.

Keep `${CLAUDE_SKILL_DIR}/references/performance-plus.md` out of routine reviews. Load it for a requested deep performance review; its more involved changes can also help investigate an established performance problem.


## Core Instructions

- iOS 27 exists, and is the default deployment target for new apps.
- Xcode 27.1 and the corresponding 27.1 platform releases are available. Do not dismiss their APIs as speculative.
- iPhone Duo is a folding iPhone, with a small outer display and a large inner display.
- Check the project's deployment target before applying version-specific advice. Use newer runtime APIs only when the target supports them or within an appropriate `#available` check. Keep the existing target unchanged. Requirements tied to an Xcode version also need that toolchain; a runtime check cannot make an older SDK recognize a new declaration.
- Target Swift 6.4 or later, using modern Swift concurrency.
- As a SwiftUI developer, the user will want to avoid UIKit unless requested.
- Do not introduce third-party frameworks without asking first.
- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file.
- Use a consistent project structure, with folder layout determined by app features.


## Output Format

Organize findings by file. For each issue:

1. State the file and relevant line(s).
2. Name the rule being violated (e.g., "Use `foregroundStyle()` instead of `foregroundColor()`").
3. Show a brief before/after code fix.

Skip files with no issues. End with a prioritized summary of the most impactful changes to make first.

Example output:

### ContentView.swift

**Line 12: Use `foregroundStyle()` instead of `foregroundColor()`.**

```swift
// Before
Text("Hello").foregroundColor(.red)

// After
Text("Hello").foregroundStyle(.red)
```

**Line 24: Give the button a descriptive label so VoiceOver explains its action.**

```swift
// Before
Button(action: addUser) {
    Image(systemName: "plus")
}

// After
Button("Add User", systemImage: "plus", action: addUser)
```

**Line 31: Avoid `Binding(get:set:)` in view body – bind to the property directly and move the setter's side effect into `onChange()`.**

```swift
// Before
TextField("Username", text: Binding(
    get: { model.username },
    set: { model.username = $0; model.save() }
))

// After
TextField("Username", text: $model.username)
    .onChange(of: model.username) {
        model.save()
    }
```

Use `onChange()` when saving should follow changes to the username, including programmatic changes. Equal-value assignments will not trigger a save.

### Summary

1. **Accessibility (high):** The add button needs an “Add User” label; the symbol’s default label does not explain what will be added.
2. **Deprecated API (medium):** `foregroundColor()` on line 12 should be `foregroundStyle()`.
3. **Data flow (medium):** The manual binding on line 31 is fragile and harder to maintain.

End of example.


## References

- `${CLAUDE_SKILL_DIR}/references/accessibility.md` - Dynamic Type, VoiceOver, Reduce Motion, and other accessibility requirements.
- `${CLAUDE_SKILL_DIR}/references/api.md` - updating code for modern API, the deprecated code it replaces, and iOS 27 toolbars.
- `${CLAUDE_SKILL_DIR}/references/design.md` - guidance for building accessible apps that meet Apple's Human Interface Guidelines.
- `${CLAUDE_SKILL_DIR}/references/hygiene.md` - making code compile cleanly and be maintainable in the long term.
- `${CLAUDE_SKILL_DIR}/references/localization.md` - localizable text, resource bundles, and locale-aware presentation.
- `${CLAUDE_SKILL_DIR}/references/navigation.md` - navigation using `NavigationStack`/`NavigationSplitView`, plus alerts, confirmation dialogs, and sheets.
- `${CLAUDE_SKILL_DIR}/references/performance.md` - optimizing SwiftUI code for maximum performance.
- `${CLAUDE_SKILL_DIR}/references/performance-plus.md` - deeper restructuring to narrow what views depend on, reserved for deep performance reviews or diagnosed performance issues.
- `${CLAUDE_SKILL_DIR}/references/resizability.md` - changing window sizes, adaptive bars, safe areas, and iPhone Duo layouts.
- `${CLAUDE_SKILL_DIR}/references/data.md` - data flow, shared state, and property wrappers.
- `${CLAUDE_SKILL_DIR}/references/swift.md` - tips on writing modern Swift code, including using Swift Concurrency effectively.
- `${CLAUDE_SKILL_DIR}/references/views.md` - view structure, composition, animation, reordering, and swipe actions.