mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
chore: bump version to 27.0.0
Axiom leaves beta for the OS 27 cycle. - iPhone Duo hub in axiom-swiftui, with fold, pose, and camera guidance folded into layout, presentation, adaptive-layout, and camera skills (276 skills, up from 275) - Every 27-cycle claim re-verified against the Xcode 27.0 RC, plus 27.0 SDK coverage for AVFoundation, PhotoKit, ScreenCaptureKit, Background Assets, XCUITest VoiceOver, and TextKit - UIDesignRequiresCompatibility is ignored for 27-SDK builds on OS 27; the Liquid Glass skill and auditor are corrected to match - iOS 26 navigation bar subtitles in SwiftUI and UIKit, with hook routing - Authoring metadata footers removed from skill files - Cursor variant and MCP bundle regenerated Gates: test:full both phases, version parity across all 9 files, unit suite 488/488, hook tests 144, MCP tests 181, cross-refs 333 files / 27 suites, router integrity, VitePress dead-link validated.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "axiom",
|
||||
"version": "27.0.0-beta.54",
|
||||
"version": "27.0.0",
|
||||
"source": "./.claude-plugin/plugins/axiom",
|
||||
"description": "Battle-tested Claude Code skills, agents, and tools for modern xOS (iOS, iPadOS, macOS, watchOS, tvOS) development",
|
||||
"author": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "axiom",
|
||||
"version": "27.0.0-beta.54",
|
||||
"version": "27.0.0",
|
||||
"description": "Battle-tested Claude Code skills, agents, and tools for modern xOS (iOS, iPadOS, macOS, watchOS, tvOS) development",
|
||||
"author": {
|
||||
"name": "Charles Wiltgen"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "axiom",
|
||||
"version": "27.0.0-beta.54",
|
||||
"version": "27.0.0",
|
||||
"description": "Battle-tested Claude Code skills, agents, and tools for modern xOS (iOS, iPadOS, macOS, watchOS, tvOS) development",
|
||||
"author": "Charles Wiltgen",
|
||||
"license": "MIT",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"name": "axiom-swiftui",
|
||||
"description": "Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features."
|
||||
"description": "Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features, iPhone Duo."
|
||||
},
|
||||
{
|
||||
"name": "axiom-data",
|
||||
|
||||
@@ -20,7 +20,7 @@ You are an iOS development assistant with access to 26 specialized Axiom skills
|
||||
|
||||
- **axiom-accessibility** — Use when fixing or auditing ANY accessibility issue — VoiceOver, Dynamic Type, color contrast, touch targets, WCAG compliance, App Store accessibility review.
|
||||
- **axiom-design** — Use when making design decisions, implementing HIG patterns, Liquid Glass, SF Symbols, typography, or structuring app entry points and authentication flows.
|
||||
- **axiom-swiftui** — Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features.
|
||||
- **axiom-swiftui** — Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features, iPhone Duo.
|
||||
- **axiom-uikit** — Use when bridging UIKit and SwiftUI, modernizing UIKit apps (scene lifecycle, resizability), debugging Auto Layout, Combine, TextKit, PencilKit, or UIKit animations.
|
||||
|
||||
### Code Quality
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
27.0.0-beta.54
|
||||
275
|
||||
27.0.0
|
||||
276
|
||||
42
|
||||
17
|
||||
|
||||
@@ -7,7 +7,7 @@ Battle-tested skills, agents, and tools for modern Apple OS development — Swif
|
||||
Axiom gives AI coding assistants deep Apple OS development expertise — the kind that prevents data loss from bad migrations, catches memory leaks before users complain, and stops you from spending 30 minutes debugging a zombie xcodebuild process.
|
||||
|
||||
<!-- AXIOM_STATS_BEGIN — auto-maintained by scripts/set-version.js; do not hand-edit -->
|
||||
- **275 skills** covering UI, data, concurrency, performance, networking, accessibility, and more
|
||||
- **276 skills** covering UI, data, concurrency, performance, networking, accessibility, and more
|
||||
- **42 agents** that autonomously scan for issues (memory leaks, concurrency violations, build problems)
|
||||
- **17 commands** for quick audits and diagnostics
|
||||
<!-- AXIOM_STATS_END -->
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "axiom",
|
||||
"version": "27.0.0-beta.54",
|
||||
"version": "27.0.0",
|
||||
"description": "Battle-tested Cursor skills, agents, commands, hooks, and MCP for modern Apple platform development.",
|
||||
"author": {
|
||||
"name": "Charles Wiltgen",
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Axiom for Cursor
|
||||
|
||||
A native Cursor plugin for modern Apple platform development, generated from Axiom 27.0.0-beta.54.
|
||||
A native Cursor plugin for modern Apple platform development, generated from Axiom 27.0.0.
|
||||
|
||||
## Included
|
||||
|
||||
|
||||
Generated
+10
@@ -216,6 +216,16 @@ Run all 10 detection patterns. For every grep match, use Read to verify the surr
|
||||
**Verify**: Read matching files; flag synchronous patterns and missing `Task { ... }` wrappers.
|
||||
**Fix**: `let image = try await item.loadTransferable(type: Data.self)`.
|
||||
|
||||
### Pattern 11: 27-Cycle Capture Property Set While Its Automatic Flag Is On (HIGH/MEDIUM)
|
||||
|
||||
**Issue**: the 27 capture controls ship in pairs — an `automatically…` flag that defaults to system control, plus the value itself. Assigning the value while the flag is still `true` raises `NSInvalidArgumentException`. That is an ObjC exception, not a Swift error: `try`/`catch` does not catch it and the app terminates.
|
||||
**Search**:
|
||||
- `enabledExposureSignals\s*=` — needs `automaticallyEnablesExposureSignals\s*=\s*false` first
|
||||
- `isLowLightVideoNoiseReductionEnabled\s*=` (on `AVCaptureConnection`) — needs `automaticallyEnablesLowLightVideoNoiseReduction\s*=\s*false` first
|
||||
- `isCinematicVideoMetadataCaptureEnabled\s*=` (on `AVCaptureMovieFileOutput`) — needs `automaticallyAdjustsCinematicVideoMetadataCaptureEnabled\s*=\s*false` first
|
||||
**Verify**: Read matching files; confirm the paired flag is set to `false` before the value, and that any `enabledExposureSignals` assignment sits between `lockForConfiguration()` and `unlockForConfiguration()` — without the lock it raises `NSGenericException` instead.
|
||||
**Fix**: `device.automaticallyEnablesExposureSignals = false` then `device.enabledExposureSignals = [.document]`; same shape for the other two pairs.
|
||||
|
||||
## Phase 3: Reason About Capture Completeness
|
||||
|
||||
Using the Capture Map from Phase 1 and your domain knowledge, check for what's *missing* — not just what's wrong.
|
||||
|
||||
Generated
+1
-1
@@ -100,7 +100,7 @@ Run these only when their framework signals are present in the codebase:
|
||||
| `NavigationStack` or `sheet(` or `TabView` | ux-flow-auditor |
|
||||
| `FileManager` or `UserDefaults` or `.documentsDirectory` | storage-auditor |
|
||||
| `XCTestCase` or `@Test` or `@Suite` | testing-auditor |
|
||||
| `.glassBackgroundEffect` or `GlassEffectContainer` | liquid-glass-auditor |
|
||||
| `.glassEffect(` or `GlassEffectContainer` or `UIBlurEffect` or `UIVisualEffectView` or `NSVisualEffectView`, or `UIDesignRequiresCompatibility` present in an Info.plist | liquid-glass-auditor |
|
||||
| Screenshots folder exists (`Screenshots/` or `marketing/`) | screenshot-validator |
|
||||
|
||||
### User Exclusions
|
||||
|
||||
+85
-79
@@ -13,7 +13,7 @@ is_background: true
|
||||
|
||||
# Liquid Glass Auditor Agent
|
||||
|
||||
You are an expert at identifying Liquid Glass adoption opportunities AND adoption gaps — both surfaces where the iOS 26+ visual treatment isn't yet applied AND adoption-completeness issues like ungated effects on older OS, wrong variant for content type (Regular vs Clear), nested glass causing visual muddiness, and missing tint discipline on primary actions.
|
||||
You are an expert at identifying Liquid Glass adoption opportunities AND adoption gaps — both surfaces where the iOS 26+ visual treatment isn't yet applied AND adoption-completeness issues like unstyled pre-26 fallbacks, wrong variant for content type (Regular vs Clear), nested glass causing visual muddiness, positionally placed primary actions, and an app-wide `UIDesignRequiresCompatibility` opt-out.
|
||||
|
||||
## Note on Audit Framing
|
||||
|
||||
@@ -36,10 +36,11 @@ Skip: `*Tests.swift`, `*Previews.swift`, `*/Pods/*`, `*/Carthage/*`, `*/.build/*
|
||||
### Step 1: Identify Deployment Target and Availability Discipline
|
||||
|
||||
```
|
||||
Glob: **/*.swift, **/*.xcconfig, **/Info.plist
|
||||
Glob: **/*.swift, **/*.xcconfig, **/*.plist, **/project.pbxproj
|
||||
Grep for:
|
||||
- `IPHONEOS_DEPLOYMENT_TARGET`, `MACOSX_DEPLOYMENT_TARGET` — deployment target
|
||||
- `if #available\(iOS\s+26`, `if #available\(macOS\s+15`, `if #available\(macOS\s+26` — availability gates for Liquid Glass
|
||||
- `UIDesignRequiresCompatibility` in `.plist` files — app-wide opt-out from the new design; read the value. A hit only as an `INFOPLIST_KEY_UIDesignRequiresCompatibility` build setting is likely inert (Xcode's build system maps no such setting into Info.plist), so report it as unconfirmed rather than as an active opt-out
|
||||
- `(if|guard) #available\(iOS\s+26`, `(if|guard) #available\(macOS\s+26` — availability gates for Liquid Glass
|
||||
- `@available\(iOS\s+26`, `@available\(macOS\s+26` — type/method-level availability
|
||||
```
|
||||
|
||||
@@ -50,20 +51,21 @@ Grep for:
|
||||
- `UIBlurEffect`, `UIVisualEffectView` — UIKit blur (legacy)
|
||||
- `NSVisualEffectView` — AppKit blur (legacy)
|
||||
- `\.ultraThinMaterial`, `\.thinMaterial`, `\.regularMaterial`, `\.thickMaterial`, `\.ultraThickMaterial`, `\.bar` — SwiftUI Material (legacy on iOS 26+)
|
||||
- `\.background\(\.material`, `\.background\(\.regularMaterial` — Material as background
|
||||
- `\.background\(\.(ultraThin|thin|regular|thick|ultraThick)Material` — Material as background
|
||||
- `\.blur\(radius:` — explicit blur (intentional or migration candidate)
|
||||
- `\.background\(\.ultraThin` — material backgrounds
|
||||
```
|
||||
|
||||
### Step 3: Identify Existing Glass Adoption
|
||||
|
||||
```
|
||||
Grep for:
|
||||
- `\.glassEffect\(` — glass on a view
|
||||
- `\.glassBackgroundEffect\(` — glass as a background
|
||||
- `\.glassBackgroundEffect\(in:\s*\.clear` — Clear variant explicit
|
||||
- `\.interactive\(\)` — interactive feedback on glass
|
||||
- `\.glassEffect\(` — glass on a view (iOS, iPadOS, macOS, tvOS, watchOS 26)
|
||||
- `\.glassEffect\(\.clear` — Clear variant explicit
|
||||
- `\.interactive\(` — interactive glass (`Glass.interactive()`, as in `.glassEffect(.regular.interactive())`)
|
||||
- `GlassEffectContainer` — grouped glass surfaces
|
||||
- `\.buttonStyle\(\.glass` — glass button styles (`.glass`, `.glass(.clear)`, `.glassProminent`)
|
||||
- `\.tint\(` paired with glass surfaces
|
||||
- `\.glassBackgroundEffect\(` — the visionOS glass API; it doesn't exist on iOS, so count it only for visionOS targets
|
||||
```
|
||||
|
||||
### Step 4: Identify Toolbar, Tab, and Search Surface
|
||||
@@ -71,10 +73,11 @@ Grep for:
|
||||
```
|
||||
Grep for:
|
||||
- `\.toolbar\s*\{`, `ToolbarItem\(`, `ToolbarItemGroup\(` — toolbar surface
|
||||
- `Spacer\(\.fixed\)`, `Spacer\(\.flexible\)` — toolbar grouping
|
||||
- `ToolbarSpacer\(` — toolbar grouping between separate `ToolbarItem`s (iOS 26)
|
||||
- `placement:\s*\.(confirmationAction|cancellationAction|primaryAction|topBarLeading|topBarTrailing|navigationBarLeading|navigationBarTrailing)` — how primary and dismiss actions are placed
|
||||
- `\.buttonStyle\(\.borderedProminent\)`, `\.buttonStyle\(\.bordered\)` — button styles
|
||||
- `TabView\(` — tab containers
|
||||
- `\.tabRole\(\.search\)` — search-tab role (iOS 18+)
|
||||
- `role:\s*\.search` — search tab, `Tab(role: .search)` (iOS 18+)
|
||||
- `NavigationStack\(`, `NavigationSplitView\(` — navigation containers
|
||||
- `\.searchable\(` — search field placements
|
||||
```
|
||||
@@ -94,16 +97,17 @@ Read 1-2 representative view files (root container / navigation / a primary scre
|
||||
- Whether existing blurs/materials are gated behind `if #available(iOS 26, *)`
|
||||
- Whether glass adoption follows Regular vs Clear variant guidance
|
||||
- Whether nested view hierarchies stack multiple glass effects
|
||||
- Whether primary actions use `.tint()` for prominence
|
||||
- Whether sheet and editor toolbars place commit and dismiss actions semantically
|
||||
|
||||
### Output
|
||||
|
||||
Write a brief **Visual Treatment Map** (5-10 lines) summarizing:
|
||||
- Deployment target (and whether iOS 26+ glass APIs are reachable without availability checks)
|
||||
- Design opt-out: `UIDesignRequiresCompatibility` absent / NO / YES
|
||||
- Existing legacy effect surface (UIBlurEffect / NSVisualEffectView / `.material` count)
|
||||
- Existing glass adoption count (`.glassEffect`, `.glassBackgroundEffect`)
|
||||
- Toolbar surface (number of toolbar definitions, primary-action discipline)
|
||||
- Tab/search structure (TabView with `.tabRole(.search)` / NavigationSplitView with `.searchable` / older patterns)
|
||||
- Existing glass adoption count (`.glassEffect`, glass button styles; `.glassBackgroundEffect` in visionOS targets only)
|
||||
- Toolbar surface (number of toolbar definitions, semantic placement of primary and dismiss actions)
|
||||
- Tab/search structure (TabView with `Tab(role: .search)` / NavigationSplitView with `.searchable` / older patterns)
|
||||
- Custom-container surfaces (Cards / Galleries / Overlays count)
|
||||
- Availability discipline (`if #available(iOS 26)` gates present / absent / partial)
|
||||
|
||||
@@ -115,82 +119,80 @@ Run all 7 detection patterns. For every grep match, use Read to verify the surro
|
||||
|
||||
### Pattern 1: Migration from Old Blur Effects (HIGH/MEDIUM)
|
||||
|
||||
**Opportunity**: `UIBlurEffect`, `NSVisualEffectView`, `.ultraThinMaterial` on iOS 26+ deployment can move to `.glassEffect()`/`.glassBackgroundEffect()`.
|
||||
**Opportunity**: Blur and material on surfaces that float over content (overlays, control clusters, custom bars) can move to glass on iOS 26+.
|
||||
**Search**:
|
||||
- `UIBlurEffect`, `UIVisualEffectView`
|
||||
- `NSVisualEffectView`
|
||||
- `\.ultraThinMaterial`, `\.regularMaterial`, `\.thickMaterial`, `\.bar`
|
||||
- `\.background\(\.material`
|
||||
**Verify**: Read matching files; if deployment target is iOS 26+ with no `if #available` gate, this is a direct replacement candidate. If lower deployment target, recommend gating the new glass behind `if #available(iOS 26, *)` while keeping old material as fallback.
|
||||
**Recommendation**:
|
||||
**Verify**: Read matching files. Flag only surfaces floating over content; material on a content-layer background (a card in a list, a section background) stays material. If deployment target is iOS 26+ with no `if #available` gate, it's a direct replacement candidate; if lower, gate the glass behind `if #available(iOS 26, *)` and keep the material as the fallback.
|
||||
**Recommendation** (SwiftUI):
|
||||
```swift
|
||||
if #available(iOS 26, *) {
|
||||
view.glassBackgroundEffect()
|
||||
content.glassEffect(.regular, in: .rect(cornerRadius: 16))
|
||||
} else {
|
||||
view.background(.ultraThinMaterial)
|
||||
content.background(.ultraThinMaterial, in: .rect(cornerRadius: 16))
|
||||
}
|
||||
```
|
||||
For UIKit, use `UIVisualEffectView(effect: UIGlassEffect())` (iOS 26); for AppKit, `NSGlassEffectView` with its `contentView` (macOS 26).
|
||||
|
||||
### Pattern 2: Toolbar Modernization (HIGH/MEDIUM)
|
||||
### Pattern 2: Toolbar Modernization (MEDIUM/MEDIUM)
|
||||
|
||||
**Opportunity**: Toolbars without `.buttonStyle(.borderedProminent)` on primary actions, or without `Spacer(.fixed)` grouping, miss the iOS 26 toolbar refinements.
|
||||
**Opportunity**: On iOS 26 the toolbar styles actions by semantic placement: a `.confirmationAction` gets prominent glass automatically and a `.cancellationAction` gets standard glass (`axiom-swiftui (skills/26-ref.md)`, ToolbarItemGroup). Toolbars that hand-place Save/Done/Cancel with positional placements miss that treatment. Items in one `ToolbarItemGroup` share a single glass pill, so actions that should read as separate groups need separate `ToolbarItem`s with `ToolbarSpacer` between them.
|
||||
**Search**:
|
||||
- `\.toolbar\s*\{` paired with no `\.borderedProminent` in the same block
|
||||
- `ToolbarItem\(` placement followed by another `ToolbarItem\(` with no `Spacer\(\.fixed\)` between
|
||||
**Verify**: Read matching files; flag toolbars where the primary action (e.g., Save, Share, Done) is plain `Button` rather than `.borderedProminent` and where similar items lack visual grouping.
|
||||
**Recommendation**:
|
||||
- `\.toolbar\s*\{` blocks in sheets or editors that place Save/Done/Cancel with `\.topBarTrailing` / `\.topBarLeading` (or the deprecated `\.navigationBarTrailing` / `\.navigationBarLeading`) instead of `\.confirmationAction` / `\.cancellationAction`
|
||||
- `ToolbarItemGroup\(` containing `Spacer\(\)` — items the author wanted visually separated inside one shared pill
|
||||
**Verify**: Read matching files; flag sheet/editor toolbars whose commit and dismiss actions use positional placements, and groups whose items should read as separate clusters.
|
||||
**Recommendation** (see `axiom-swiftui (skills/toolbars.md)` Patterns 2 and 5):
|
||||
```swift
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
Button("Cancel") { ... }
|
||||
Spacer(.fixed)
|
||||
Button("Save") { ... }.buttonStyle(.borderedProminent).tint(.accentColor)
|
||||
}
|
||||
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
|
||||
ToolbarItem(placement: .confirmationAction) { Button("Save") { save() } }
|
||||
}
|
||||
```
|
||||
Separate groups of ordinary items with `ToolbarSpacer(.fixed)` between `ToolbarItem`s. A plain `Spacer()` there doesn't compile; it only works inside a `ToolbarItem` or `ToolbarItemGroup`.
|
||||
|
||||
### Pattern 3: Custom Containers Without Glass (MEDIUM/MEDIUM)
|
||||
|
||||
**Opportunity**: Custom card/gallery/overlay views without `.glassBackgroundEffect()` miss the depth and material that iOS 26 chrome provides.
|
||||
**Opportunity**: Custom floating card/overlay/control views without glass miss the depth and material that iOS 26 chrome provides. Glass belongs on controls and navigation layered over content, not on content itself.
|
||||
**Search**:
|
||||
- `struct\s+\w*(Card|Container|Overlay|Sheet|Gallery|Pane|Tile)\w*\s*:\s*View`
|
||||
- Verify that the view's body doesn't already include `.glassEffect` or `.glassBackgroundEffect`
|
||||
**Verify**: Read matching files; flag visible-chrome containers (not text-only labels). Skip purely structural containers (HStack/VStack with no visual appearance).
|
||||
**Recommendation**: Apply `.glassBackgroundEffect()` (Regular variant for content surfaces) or `.glassBackgroundEffect(in: .clear)` (for media overlays).
|
||||
- Verify that the view's body doesn't already include `.glassEffect`
|
||||
**Verify**: Read matching files; flag floating visible-chrome surfaces (overlays, control clusters), not text-only labels or list content. Skip purely structural containers (HStack/VStack with no visual appearance).
|
||||
**Recommendation**: Apply `.glassEffect(.regular, in: .rect(cornerRadius: 16))` for surfaces over ordinary content, or `.glassEffect(.clear, in: .rect(cornerRadius: 16))` over media. Group nearby glass elements in a `GlassEffectContainer`.
|
||||
|
||||
### Pattern 4: Search Pattern Modernization (MEDIUM/MEDIUM)
|
||||
|
||||
**Opportunity**: `.searchable()` outside `NavigationSplitView`, or `TabView` without a `.tabRole(.search)` tab, miss the platform-aligned search UX iOS 26 ships with.
|
||||
**Opportunity**: `.searchable()` outside `NavigationSplitView`, or `TabView` without a `Tab(role: .search)` tab, miss the platform-aligned search UX iOS 26 ships with.
|
||||
**Search**:
|
||||
- `\.searchable\(` not inside a `NavigationSplitView` block
|
||||
- `TabView\(` with no `\.tabRole\(\.search\)` in any of its tabs
|
||||
- `TabView` with no `role:\s*\.search` in any of its `Tab`s
|
||||
**Verify**: Read matching files; flag only when the screen has a search-as-primary-action pattern.
|
||||
**Recommendation**: For tab-based apps, dedicate one tab with `.tabRole(.search)`; for split-view apps, place `.searchable` on the sidebar.
|
||||
**Recommendation**: For tab-based apps, dedicate one tab with `Tab(role: .search)`; for split-view apps, place `.searchable` on the sidebar.
|
||||
|
||||
### Pattern 5: Glass-on-Glass Layering (MEDIUM/HIGH)
|
||||
|
||||
**Opportunity**: Nested views with multiple glass effects layer translucency, producing visual muddiness. Apply glass only to the outermost surface.
|
||||
**Search**:
|
||||
- `\.glassEffect\(` or `\.glassBackgroundEffect\(` — count occurrences
|
||||
- `\.glassEffect\(` — count occurrences
|
||||
- For each match, check if the parent view in the same file also applies a glass effect
|
||||
**Verify**: Read matching files; trace the view hierarchy. If a card with `.glassBackgroundEffect()` is inside an overlay with `.glassBackgroundEffect()`, flag the inner one.
|
||||
**Recommendation**: Remove the inner glass effect; keep only the outermost container's glass surface.
|
||||
**Verify**: Read matching files; trace the view hierarchy. If a card with `.glassEffect()` is inside an overlay with `.glassEffect()`, flag the inner one.
|
||||
**Recommendation**: Remove the inner glass effect and keep only the outermost surface's glass. For sibling glass elements that sit close together, wrap them in a `GlassEffectContainer` instead.
|
||||
|
||||
### Pattern 6: Tinting Opportunities (LOW/MEDIUM)
|
||||
|
||||
**Opportunity**: `.buttonStyle(.borderedProminent)` without `.tint()` misses the color prominence that signals primary action.
|
||||
**Opportunity**: A prominent button already fills with the accent color, so it needs no `.tint()` for prominence. The opportunity is semantic color: a prominent button whose meaning differs from the brand accent (a confirmation that should read green, a destructive action) still shows the accent color.
|
||||
**Search**:
|
||||
- `\.borderedProminent` not followed by `\.tint\(` on the same view chain
|
||||
**Verify**: Read matching files; confirm the prominent button is a primary action (Save / Submit / Continue), not a destructive one.
|
||||
**Recommendation**: `.buttonStyle(.borderedProminent).tint(.accentColor)` — or a semantic tint like `.tint(.green)` for confirmation, `.tint(.red)` for destructive.
|
||||
- `\.borderedProminent` or `\.glassProminent` not followed by `\.tint\(` on the same view chain
|
||||
**Verify**: Read matching files; flag only prominent buttons whose action carries a meaning the accent color doesn't convey. For destructive actions, prefer `Button(role: .destructive)` over a hand-picked red.
|
||||
**Recommendation**: A semantic tint such as `.tint(.green)` for a confirmation that must stand apart from the accent color. Don't add `.tint(.accentColor)`; it changes nothing.
|
||||
|
||||
### Pattern 7: Missing .interactive() on Custom Controls (LOW/LOW)
|
||||
|
||||
**Opportunity**: Custom buttons or interactive surfaces with glass effects but no `.interactive()` lose automatic press-state visual feedback.
|
||||
**Opportunity**: Custom tappable surfaces with glass but no interactive glass lose the press-state feedback the material provides.
|
||||
**Search**:
|
||||
- `\.glassEffect\(` or `\.glassBackgroundEffect\(` on a Button/control without `\.interactive\(\)` nearby
|
||||
**Verify**: Read matching files; flag interactive surfaces (Button, custom hit-testing views), not static cards.
|
||||
**Recommendation**: Append `.interactive()` after `.glassEffect()` so press states animate the glass surface.
|
||||
- `\.glassEffect\(` on a Button/control or tap-handling view without `\.interactive\(` in its `Glass` argument
|
||||
**Verify**: Read matching files; flag interactive surfaces (custom hit-testing views, gesture targets), not static cards. A standard `Button` is better served by `.buttonStyle(.glass)`.
|
||||
**Recommendation**: `interactive()` is a method on `Glass`, not a view modifier: write `.glassEffect(.regular.interactive())`. For buttons, use `.buttonStyle(.glass)` or `.buttonStyle(.glassProminent)`.
|
||||
|
||||
## Phase 3: Reason About Adoption Completeness
|
||||
|
||||
@@ -198,17 +200,17 @@ Using the Visual Treatment Map from Phase 1 and your domain knowledge, check for
|
||||
|
||||
| Question | What it detects | Why it matters |
|
||||
|----------|----------------|----------------|
|
||||
| If deployment target is below iOS 26, is every `.glassEffect()` / `.glassBackgroundEffect()` call gated behind `if #available(iOS 26, *)`? | Build/runtime mismatch | Calling iOS 26-only API on iOS 25 crashes at runtime; without `#available` the Xcode warning is the only signal |
|
||||
| For glass surfaces over photos/videos/maps (media-heavy contexts), is the Clear variant (`.glassBackgroundEffect(in: .clear)`) chosen rather than Regular? | Visual muddiness over media | Regular adds tint that distorts the underlying photo/video color; Clear preserves accuracy |
|
||||
| Is `UIDesignRequiresCompatibility` set to YES? | App-wide design opt-out | iOS 26 users see compatibility mode, so no adoption finding is visible to them. The system ignores the key once the app builds with the 27 SDK and runs on OS 27 (already true if the project builds with Xcode 27), so the new design goes live on OS 27 whether or not it was reviewed. See `axiom-design (skills/liquid-glass.md)`, Backward Compatibility |
|
||||
| If deployment target is below iOS 26, does every `if #available(iOS 26, *)` around a glass call have a designed `else` branch (e.g. `.background(.ultraThinMaterial)`) rather than an empty or unstyled one? | Bare pre-26 surfaces | The compiler already refuses an ungated `.glassEffect()` below the deployment target; what it can't catch is an `else` branch that ships an unstyled view to iOS 18 users |
|
||||
| For glass surfaces over photos/videos/maps (media-heavy contexts), is the Clear variant (`.glassEffect(.clear)`) chosen rather than Regular? | Visual muddiness over media | Regular adds tint that distorts the underlying photo/video color; Clear preserves accuracy |
|
||||
| For glass adoption, has the team verified contrast against accessibility audit baseline (text-on-glass meets WCAG)? | Accessibility regression | Glass surfaces can drop text contrast below 4.5:1; readers with low vision lose readability |
|
||||
| Are nested visual surfaces flattened so only the outermost view applies glass? | Glass-on-glass mud | Stacked translucency turns into haze; the visual hierarchy reads as "everything is glass" instead of structured layers |
|
||||
| For tab-based apps, does at least one tab use `.tabRole(.search)` to take advantage of iOS 26's bottom-aligned search? | Off-platform search UX | Custom search bars feel out of place against the system's bottom-aligned search treatment |
|
||||
| Are toolbar primary actions distinguished via `.buttonStyle(.borderedProminent).tint()` vs secondary actions as plain `Button`? | Primary action invisibility | Without prominence + tint, all toolbar items read as equally weighted; users guess which is the primary action |
|
||||
| If the codebase mixes legacy `.material` with new `.glassBackgroundEffect()` on the same screen, is there a visual review of the result? | Material/Glass mismatch | Regular + Clear variants combined with Material on the same screen reads as inconsistent design language |
|
||||
| Are `.glassEffect()` / `.glassBackgroundEffect()` adoption sites covered by visual regression tests (snapshot or screenshot tests on iOS 26 and iOS 25)? | Regression risk | Glass adoption can shift layout (different padding); without snapshot tests, subtle visual regressions ship |
|
||||
| For custom controls with glass surfaces, is `.interactive()` applied so press states animate the glass material itself (not a separate overlay)? | Inert glass feedback | Without `.interactive()`, the glass surface stays static during taps; users get no material-aware feedback |
|
||||
| For tab-based apps, does at least one tab use `Tab(role: .search)` to take advantage of iOS 26's bottom-aligned search? | Off-platform search UX | Custom search bars feel out of place against the system's bottom-aligned search treatment |
|
||||
| Do sheet and editor toolbars place commit and dismiss actions with `.confirmationAction` / `.cancellationAction`, so the system applies prominent and standard glass? | Primary action invisibility | Positional placements get no automatic prominence; all toolbar items read as equally weighted and users guess which is the primary action |
|
||||
| If the codebase mixes legacy `.material` with new `.glassEffect()` on the same screen, is there a visual review of the result? | Material/Glass mismatch | Regular + Clear variants combined with Material on the same screen reads as inconsistent design language |
|
||||
| Are `.glassEffect()` adoption sites covered by visual regression tests (snapshot or screenshot tests on iOS 26 and iOS 18)? | Regression risk | Glass adoption can shift layout (different padding); without snapshot tests, subtle visual regressions ship |
|
||||
| For custom controls with glass surfaces, is interactive glass (`.glassEffect(.regular.interactive())`) used so press states animate the material itself (not a separate overlay)? | Inert glass feedback | Without interactive glass, the surface stays static during taps; users get no material-aware feedback |
|
||||
| Has the team established a glass-adoption rubric (which view types adopt glass, which keep solid surfaces) so adoption stays consistent across new screens? | Inconsistent adoption | Without a rubric, half the cards adopt glass and half don't; the design feels random |
|
||||
| For mixed-deployment apps (iOS 25 + iOS 26 users), is there a fallback that doesn't look "broken" on older OS — e.g., `.background(.ultraThinMaterial)` for iOS 25 users? | Pre-iOS 26 fallback | Calling unavailable APIs is a build-time guard, but the visual fallback experience needs design review too |
|
||||
|
||||
Require evidence from the Phase 1 map — don't speculate without reading the code.
|
||||
|
||||
@@ -218,16 +220,17 @@ Bump priority for these combinations:
|
||||
|
||||
| Finding A | + Finding B | = Compound | Priority |
|
||||
|-----------|------------|-----------|----------|
|
||||
| Old `.material` background (Pattern 1) | iOS 26+ deployment target with no `if #available` gate | Direct replacement, ship-ready | HIGH |
|
||||
| `UIDesignRequiresCompatibility` = YES (Phase 3) | Any adoption opportunity (Patterns 1–7) | Hidden from iOS 26 users but shipped unreviewed on OS 27 by the first 27-SDK build (already live if the project builds with Xcode 27); finish adoption before or with that SDK move | HIGH |
|
||||
| Old `.material` on a floating surface (Pattern 1) | iOS 26+ deployment target with no `if #available` gate | Direct replacement, ship-ready | HIGH |
|
||||
| Glass over media (Phase 3) | Regular variant chosen | Color distortion over photos/videos; switch to Clear immediately | HIGH |
|
||||
| Glass adoption (Pattern 1/3) | No accessibility re-check | Contrast may drop below WCAG 4.5:1; flag for accessibility-auditor follow-up | HIGH |
|
||||
| Multiple nested glass effects (Pattern 5) | Outer view also has glass | Mud; remove inner glass on every nested layer | HIGH |
|
||||
| Toolbar without `.borderedProminent` (Pattern 2) | Primary action present (Save / Submit) | Primary action invisible; users guess | MEDIUM |
|
||||
| `.borderedProminent` (Pattern 6) | No `.tint()` | Tinting opportunity matrix; pair with brand color | MEDIUM |
|
||||
| `.searchable` (Pattern 4) | TabView with no `.tabRole(.search)` | Off-platform search UX; promote one tab | MEDIUM |
|
||||
| Custom container (Pattern 3) | View has visible chrome (RoundedRectangle background) | Likely glass candidate; verify content type | MEDIUM |
|
||||
| Glass adoption | Pre-iOS-26 deployment target without `#available` gate | Crash on older OS; gate immediately | HIGH (becomes a safety issue) |
|
||||
| Mixed `.material` + `.glassBackgroundEffect()` on same screen | No visual review | Inconsistent design language; the screen reads as "in transition" | MEDIUM |
|
||||
| Positional commit/dismiss placement (Pattern 2) | Sheet or editor with Save / Done | Primary action invisible; use `.confirmationAction` / `.cancellationAction` | MEDIUM |
|
||||
| Prominent button whose meaning differs from the accent (Pattern 6) | No semantic `.tint()` | Confirmation reads as an ordinary accent action | LOW |
|
||||
| `.searchable` (Pattern 4) | TabView with no `Tab(role: .search)` | Off-platform search UX; promote one tab | MEDIUM |
|
||||
| Custom container (Pattern 3) | Floats over content with visible chrome (RoundedRectangle background) | Likely glass candidate; list and content cards are not | MEDIUM |
|
||||
| Glass adoption | Pre-iOS-26 deployment target with an empty or unstyled `else` branch | iOS 18 users see a bare surface where iOS 26 users see glass | MEDIUM |
|
||||
| Mixed `.material` + `.glassEffect()` on same screen | No visual review | Inconsistent design language; the screen reads as "in transition" | MEDIUM |
|
||||
| Custom interactive control with glass (Pattern 7) | Frequently tapped (button, hit area) | Missing `.interactive()` makes the surface feel inert | LOW |
|
||||
|
||||
Cross-auditor overlap notes:
|
||||
@@ -242,19 +245,20 @@ Cross-auditor overlap notes:
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Deployment target | iOS X.Y |
|
||||
| Design opt-out | `UIDesignRequiresCompatibility` absent / NO / YES (compatibility mode on 26.x; ignored on OS 27 in 27-SDK builds) |
|
||||
| Legacy effect sites | M UIBlurEffect/NSVisualEffectView/`.material` references |
|
||||
| Glass adoption sites | N `.glassEffect`/`.glassBackgroundEffect` calls |
|
||||
| Toolbar modernization | M of N toolbars use `.borderedProminent` + tint on primary action (Z%) |
|
||||
| Search alignment | TabView with `.tabRole(.search)` / NavigationSplitView `.searchable` / older pattern |
|
||||
| Glass adoption sites | N `.glassEffect` calls and glass button styles |
|
||||
| Toolbar modernization | M of N sheet/editor toolbars use `.confirmationAction` / `.cancellationAction` (Z%) |
|
||||
| Search alignment | TabView with `Tab(role: .search)` / NavigationSplitView `.searchable` / older pattern |
|
||||
| Variant discipline | Regular for content / Clear for media — followed / mixed / unaware |
|
||||
| Nesting hygiene | No glass-on-glass / some nesting / many nested |
|
||||
| Availability gating | `if #available(iOS 26)` consistent / partial / absent |
|
||||
| Pre-26 fallbacks | designed / partly unstyled / unstyled / N/A (iOS 26+ target) |
|
||||
| **Adoption** | **ADOPTED / PARTIAL / NOT ADOPTED** |
|
||||
|
||||
Scoring (adoption progress, not danger):
|
||||
- **ADOPTED**: Glass surfaces present on app chrome (toolbars, tabs, sidebars, primary containers), variant discipline followed (Regular for content, Clear for media), no glass-on-glass nesting, primary actions use `.borderedProminent` + `.tint()`, search uses `.tabRole(.search)` or split-view `.searchable`, availability gates in place where needed. The app reads as a native iOS 26 app.
|
||||
- **PARTIAL**: Some adoption (a few glass surfaces) but inconsistent — some toolbars modern and some legacy, mixed variants, some nesting, partial availability gating. The app reads as "in transition."
|
||||
- **NOT ADOPTED**: No `.glassEffect`/`.glassBackgroundEffect` adoption (only legacy blurs/materials), no toolbar modernization, no `.tabRole(.search)`. The app looks like an iOS 25 app on iOS 26 hardware.
|
||||
- **ADOPTED**: Glass surfaces present on app chrome (toolbars, tabs, sidebars, primary containers), variant discipline followed (Regular for content, Clear for media), no glass-on-glass nesting, commit and dismiss actions use semantic toolbar placements, search uses `Tab(role: .search)` or split-view `.searchable`, pre-26 fallbacks designed where needed, no compatibility opt-out. The app reads as a native iOS 26 app.
|
||||
- **PARTIAL**: Some adoption (a few glass surfaces) but inconsistent — some toolbars modern and some legacy, mixed variants, some nesting, some unstyled fallbacks. The app reads as "in transition."
|
||||
- **NOT ADOPTED**: No `.glassEffect` adoption, no toolbar modernization, no `Tab(role: .search)`. Custom surfaces still use pre-26 materials; only system chrome has glass, which it got automatically. With `UIDesignRequiresCompatibility` = YES, iOS 26 users don't see even that.
|
||||
|
||||
## Output Format
|
||||
|
||||
@@ -286,10 +290,10 @@ Scoring (adoption progress, not danger):
|
||||
**Cross-Auditor Notes**: [if overlapping with another auditor]
|
||||
|
||||
## Recommendations
|
||||
1. [Immediate adoption — HIGH-priority migrations (legacy blur on iOS 26+, primary action prominence, glass-on-glass mud, availability gates if missing)]
|
||||
2. [Short-term — MEDIUM-priority adoption (custom containers, search modernization, tinting, variant fixes over media)]
|
||||
3. [Long-term — completeness gaps from Phase 3 (accessibility re-check, snapshot tests on iOS 25 + iOS 26, glass-adoption rubric)]
|
||||
4. [Test plan — visual regression on iOS 25 fallback, accessibility contrast on glass surfaces, performance on older devices]
|
||||
1. [Immediate adoption — HIGH-priority items (compatibility opt-out with pending adoption, legacy blur on floating surfaces on iOS 26+, glass-on-glass mud, Regular glass over media)]
|
||||
2. [Short-term — MEDIUM-priority adoption (semantic toolbar placement, custom floating views, search modernization, unstyled pre-26 fallbacks); LOW items such as semantic tint]
|
||||
3. [Long-term — completeness gaps from Phase 3 (accessibility re-check, snapshot tests on iOS 18 + iOS 26, glass-adoption rubric)]
|
||||
4. [Test plan — visual regression on the iOS 18 fallback, accessibility contrast on glass surfaces, performance on older devices]
|
||||
```
|
||||
|
||||
## Output Limits
|
||||
@@ -299,14 +303,16 @@ If >100 total opportunities: Summarize by category, show only HIGH/MEDIUM detail
|
||||
|
||||
## False Positives (Not Issues)
|
||||
|
||||
- `.ultraThinMaterial` / `.regularMaterial` in code paths gated behind `if #available(iOS 25, *)` else-branch (legitimate iOS 18-25 fallback)
|
||||
- `.ultraThinMaterial` / `.regularMaterial` in the `else` branch of `if #available(iOS 26, *)` (legitimate pre-iOS 26 fallback)
|
||||
- Material on content-layer backgrounds (cards in a list, section backgrounds); glass is for surfaces floating over content
|
||||
- UIKit `UIBlurEffect` in legacy code paths the team has explicitly chosen not to migrate
|
||||
- `.blur(radius:)` used for intentional blur effects (loading states, censoring, depth-of-field), not as a glass substitute
|
||||
- Custom views that are text-only labels (no need for glass)
|
||||
- Glass effects on sibling views (not nested in a parent that also has glass)
|
||||
- `.material` backgrounds on iOS 25-only deployment targets (Liquid Glass requires iOS 26)
|
||||
- `.glassBackgroundEffect()` in visionOS targets (the visionOS glass API, not an iOS adoption gap)
|
||||
- `UIDesignRequiresCompatibility` set to NO, or absent
|
||||
- Toolbars in deeply utility-only screens where prominence is undesired (e.g., Settings detail views)
|
||||
- `.borderedProminent` without `.tint()` when the action is destructive (red default is intentional)
|
||||
- `.borderedProminent` / `.glassProminent` without `.tint()` on an ordinary primary action (the accent color is already the fill)
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
Generated
+28
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: resize-auditor
|
||||
description: "Use this agent when the user mentions window resizing support, resizable-window readiness, iPhone Mirroring compatibility, scene-lifecycle migration checking, or preparing an app for the 27-cycle resizing model."
|
||||
description: "Use this agent when the user mentions window resizing support, resizable-window readiness, iPhone Mirroring compatibility, iPhone Duo readiness, scene-lifecycle migration checking, or preparing an app for the 27-cycle resizing model."
|
||||
model: inherit
|
||||
readonly: true
|
||||
is_background: true
|
||||
@@ -13,7 +13,7 @@ is_background: true
|
||||
|
||||
# Resize Readiness Auditor Agent
|
||||
|
||||
You are an expert at detecting resize-readiness violations across UIKit code, Info.plist, and the scene manifest. The 27 cycle makes every app resizable — iPhone apps included (iPhone Mirroring on the Mac, iPhone-only apps on iPad) — and makes the scene-based life cycle mandatory. This audit finds what breaks under that model, from launch-blocking configuration to layouts and rendering surfaces that assume a fixed canvas.
|
||||
You are an expert at detecting resize-readiness violations across UIKit code, Info.plist, and the scene manifest. The 27 cycle makes every app resizable — iPhone apps included (iPhone Mirroring on the Mac, iPhone-only apps on iPad) — and makes the scene-based life cycle mandatory. This audit finds what breaks under that model, from launch-blocking configuration to layouts and rendering surfaces that assume a fixed canvas. iPhone Duo adds a two-display iPhone whose bars move to one side and whose outer display can't create windows; checks 13–15 cover it.
|
||||
|
||||
**Division of labor**: SwiftUI-side layout adaptivity (GeometryReader misuse, size-class misuse, identity loss, hardcoded breakpoints) is `swiftui-layout-auditor`'s territory. This auditor owns the UIKit, configuration, scene-lifecycle, rendering-surface, and Mirroring-input surface. Where a project mixes both, report the overlap in Cross-Auditor Notes rather than duplicating findings.
|
||||
|
||||
@@ -56,6 +56,7 @@ Grep for:
|
||||
- `traitCollection.displayScale` — correct scale source
|
||||
- `interfaceOrientation`, `UIDevice.current.orientation` — orientation reads
|
||||
- `userInterfaceIdiom` — device-identity checks
|
||||
- `safeAreaInsets`, `layoutMargins` — manual inset math (read for per-side handling)
|
||||
```
|
||||
|
||||
### Step 3: Rendering and Input Surfaces
|
||||
@@ -66,6 +67,8 @@ Grep for:
|
||||
- `SKView`, `scaleMode` — SpriteKit scenes
|
||||
- `UIPanGestureRecognizer`, `allowedScrollTypesMask` — custom pan handling
|
||||
- `UIApplicationSupportsIndirectInputEvents` in plist files
|
||||
- `UIToolbar(`, `UINavigationBar(`, `UITabBar(` — hand-instantiated bars
|
||||
- `requestSceneSessionActivation`, `activateSceneSession` — window requests
|
||||
```
|
||||
|
||||
### Output
|
||||
@@ -171,6 +174,27 @@ For every grep match, use Read to verify the surrounding context before reportin
|
||||
**Issue**: Trackpad pinch/rotate arrive as transform events with `numberOfTouches == 0`; `location(ofTouch:in:)` raises on them
|
||||
**Fix**: Detect non-touch events (e.g. in `shouldReceive(_:)`) before reading touches — axiom-uikit (skills/uikit-modernization.md)
|
||||
|
||||
### 13. Symmetric Safe-Area or Margin Math (MEDIUM — iPhone Duo)
|
||||
|
||||
**Pattern**: One inset doubled, or left and right assumed equal
|
||||
**Search**: `safeAreaInsets\.(left|right)\s*\*\s*2`, `layoutMargins\.(left|right)\s*\*\s*2`, `directionalLayoutMargins\.(leading|trailing)\s*\*\s*2`, `2\s*\*\s*[\w.]*safeAreaInsets` — Read context to confirm layout math
|
||||
**Issue**: Insets are asymmetric whenever a bar, window control, or camera sits on one side. On iPhone Duo the vertical bar sits on one side and switches sides in Split View; doubled math clips or misaligns content
|
||||
**Fix**: `view.bounds.inset(by: view.safeAreaInsets)`, or constrain each edge to `safeAreaLayoutGuide` / `layoutMarginsGuide` — axiom-swiftui (skills/iphone-duo.md)
|
||||
|
||||
### 14. Hand-Built Bars (MEDIUM — iPhone Duo)
|
||||
|
||||
**Pattern**: `UIToolbar`, `UINavigationBar`, or `UITabBar` instantiated and added to a view hierarchy instead of the bars `UINavigationController` / `UITabBarController` manage
|
||||
**Search**: `UIToolbar\(`, `UINavigationBar\(`, `UITabBar\(` — Read context: flag when added with `addSubview`; skip bars assigned to `inputAccessoryView`
|
||||
**Issue**: Built against the iOS 27.1 SDK, system-managed bars move to the side on iPhone Duo and join overflow and fold avoidance; the content of hand-built bars is ignored, so they stay put
|
||||
**Fix**: Set `toolbarItems` on the view controller inside a `UINavigationController` (`isToolbarHidden = false`) and put navigation items on `navigationItem` — axiom-swiftui (skills/iphone-duo.md, Vertical Bars)
|
||||
|
||||
### 15. Unguarded Window Requests (MEDIUM — iPhone Duo)
|
||||
|
||||
**Pattern**: Scene-activation calls that can't report failure
|
||||
**Search**: `requestSceneSessionActivation`, `activateSceneSession\(for:` — Read the `errorHandler` argument
|
||||
**Issue**: `requestSceneSessionActivation` is headed for deprecation, and a nil or empty error handler hides failures. iPhone Duo's outer display can't create windows, so requests there fail
|
||||
**Fix**: `UIApplication.shared.activateSceneSession(for: request) { error in … }` (iOS 17) with a real fallback; in menus, `UIWindowScene.ActivationAction`, which hides itself when new windows aren't available
|
||||
|
||||
## Phase 3: Reason About Resize Completeness
|
||||
|
||||
Using the Windowing Model Map and your domain knowledge, check for what's *missing* — not just what's wrong. Derive the greps from each row's API names (`sizeRestrictions`, `isInteractivelyResizing`, `stateRestorationActivity`, `deviceOwnerAuthenticationWithBiometricsOrCompanion`, `effectiveContentSize`) — "Tool Use Is Mandatory" applies here too.
|
||||
@@ -277,6 +301,8 @@ If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
|
||||
- `numberOfTouches` guarded by an event-type check (already Mirroring-safe)
|
||||
- `bounds` reads inside `layoutSubviews`/`viewDidLayoutSubviews` — that is the fix for cached geometry, not the bug
|
||||
- Device-size literals in comments, test fixtures, or design-token documentation
|
||||
- A `UIToolbar` assigned to `inputAccessoryView` — keyboard accessory bars stay on the keyboard by design
|
||||
- Inset math that already reads each side separately (`safeAreaInsets.left + safeAreaInsets.right`)
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
+19
-1
@@ -50,6 +50,8 @@ Grep for:
|
||||
- `UIScreen.main`, `UIDevice.current.orientation` — deprecated APIs
|
||||
- `.width >`, `.width <`, `.height >` — numeric breakpoints
|
||||
- `UIRequiresFullScreen` in plist files
|
||||
- `.ignoresSafeArea(` — safe-area opt-outs (read what they wrap)
|
||||
- `.safeAreaInset(edge:` — edge-pinned custom content
|
||||
```
|
||||
|
||||
### Step 3: Understand Adaptivity Strategy
|
||||
@@ -74,7 +76,7 @@ Present this map in the output before proceeding.
|
||||
|
||||
## Phase 2: Detect Known Anti-Patterns
|
||||
|
||||
Run all 10 existing detection patterns. For every grep match, use Read to verify the surrounding context before reporting — grep patterns have high recall but need contextual verification.
|
||||
Run all 12 existing detection patterns. For every grep match, use Read to verify the surrounding context before reporting — grep patterns have high recall but need contextual verification.
|
||||
|
||||
### 1. GeometryReader in Stacks Without .frame() (CRITICAL)
|
||||
|
||||
@@ -147,6 +149,20 @@ Run all 10 existing detection patterns. For every grep match, use Read to verify
|
||||
**Issue**: `containerRelativeFrame` (iOS 17+) handles relative sizing more cleanly with proper layout participation
|
||||
**Fix**: Replace `GeometryReader { geo in view.frame(width: geo.size.width * 0.5) }` with `.containerRelativeFrame(.horizontal) { w, _ in w * 0.5 }`
|
||||
|
||||
### 11. ignoresSafeArea on Interactive Content (HIGH)
|
||||
|
||||
**Pattern**: `.ignoresSafeArea()` applied to a container whose children include controls
|
||||
**Search**: `\.ignoresSafeArea\(` — Read the modified view: flag when it wraps `Button`, `Toggle`, `TextField`, `Picker`, `Slider`, `Link`, or `NavigationLink` content rather than a background (`Color`, `Image`, a gradient, or content inside `.background { }`)
|
||||
**Issue**: Controls extend under the status bar, the Dynamic Island, window controls, and — on iPhone Duo — the vertical bar and the inner camera, where people can't reach them
|
||||
**Fix**: Apply `.ignoresSafeArea()` to the background only: `content.background { Color.accentColor.ignoresSafeArea() }`
|
||||
|
||||
### 12. Hand-Built Toolbar Rows (MEDIUM)
|
||||
|
||||
**Pattern**: A row of bar-style action buttons (share, edit, add, filter, delete — what `.toolbar` placements exist for) pinned to an edge with `.safeAreaInset(edge:)`, `.overlay(alignment: .bottom)` / `.top`, or `VStack { Spacer(); HStack { Button… } }`, standing in for a toolbar
|
||||
**Search**: `\.safeAreaInset\(edge:`, `\.overlay\(alignment: \.(bottom|top)`, `Spacer\(\)` followed by `HStack` — Read context: flag only rows of two or more action buttons; content controls stay out — media transport (play/pause/skip) and scrubbers, composer fields, banners
|
||||
**Issue**: Hand-built bars never join system overflow, priorities, or placement. On iPhone Duo they can't move to the side when built against the 27.1 SDK, and they don't avoid the fold
|
||||
**Fix**: Move the buttons into `.toolbar` with semantic placements (axiom-swiftui skills/toolbars.md, skills/iphone-duo.md); keep `.safeAreaInset` for genuinely custom accessory content
|
||||
|
||||
## Phase 3: Reason About Layout Completeness
|
||||
|
||||
Using the Layout Strategy Map from Phase 1 and your domain knowledge, check for what's *missing* — not just what's wrong.
|
||||
@@ -258,6 +274,8 @@ If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
|
||||
- Size class checks that genuinely adapt layout (not inferring orientation)
|
||||
- GeometryReader with `.frame()` constraint (already safe)
|
||||
- Large fixed frames for full-screen backgrounds/images (intentional)
|
||||
- `.ignoresSafeArea()` on backgrounds, full-bleed images, maps, or video players
|
||||
- `.safeAreaInset` hosting content controls — media transport (play/pause/skip), a scrubber, a text composer, or a banner — rather than action buttons
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -22,7 +22,7 @@ You are an iOS development assistant with access to 26 specialized Axiom skills
|
||||
|
||||
- **axiom-accessibility** — Use when fixing or auditing ANY accessibility issue — VoiceOver, Dynamic Type, color contrast, touch targets, WCAG compliance, App Store accessibility review.
|
||||
- **axiom-design** — Use when making design decisions, implementing HIG patterns, Liquid Glass, SF Symbols, typography, or structuring app entry points and authentication flows.
|
||||
- **axiom-swiftui** — Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features.
|
||||
- **axiom-swiftui** — Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features, iPhone Duo.
|
||||
- **axiom-uikit** — Use when bridging UIKit and SwiftUI, modernizing UIKit apps (scene lifecycle, resizability), debugging Auto Layout, Combine, TextKit, PencilKit, or UIKit animations.
|
||||
|
||||
### Code Quality
|
||||
|
||||
+219
-214
@@ -3,13 +3,13 @@
|
||||
"files": [
|
||||
{
|
||||
"path": ".cursor-plugin/plugin.json",
|
||||
"sha256": "c030d2fae0693b56963beaf4f162377500b19429db9799b04da84adfc0528834",
|
||||
"bytes": 586
|
||||
"sha256": "eda902fecca53c937216e71bf2512286fa82c84d3743a484fa14a7fad1c63400",
|
||||
"bytes": 578
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "b84953f574f49eeaa15cf3bf0c78434d0686d9e0ddd44f7fb74d822b75bb4341",
|
||||
"bytes": 3322
|
||||
"sha256": "7cb0b0e747683c87e46096b82ec41fa606d19ea1f4d3b91ffb3af44d1e70e41b",
|
||||
"bytes": 3314
|
||||
},
|
||||
{
|
||||
"path": "agents/accessibility-auditor.md",
|
||||
@@ -28,8 +28,8 @@
|
||||
},
|
||||
{
|
||||
"path": "agents/camera-auditor.md",
|
||||
"sha256": "f393e13e997572b4ecafbefbea4126e55d0216ea220e25c5b5e0fb5bb831462f",
|
||||
"bytes": 23438
|
||||
"sha256": "f9f898687dc82943496c13f2d970d74e0a0dc98422dd6c90f3630f0173433c45",
|
||||
"bytes": 24704
|
||||
},
|
||||
{
|
||||
"path": "agents/codable-auditor.md",
|
||||
@@ -73,8 +73,8 @@
|
||||
},
|
||||
{
|
||||
"path": "agents/health-check.md",
|
||||
"sha256": "8f1530966d2e4af149675fe46da52c3be8d3eee7e06c714576158549ab619b36",
|
||||
"bytes": 9085
|
||||
"sha256": "b7028243cb61858c8789018d8bd1d1086e07efd8d8a35ca317b1dfdd50a5f827",
|
||||
"bytes": 9203
|
||||
},
|
||||
{
|
||||
"path": "agents/iap-auditor.md",
|
||||
@@ -93,8 +93,8 @@
|
||||
},
|
||||
{
|
||||
"path": "agents/liquid-glass-auditor.md",
|
||||
"sha256": "8f60b84aab3769a36975e9ec69039cbb61575af96f920bb01cff5dc8f3eee49d",
|
||||
"bytes": 20373
|
||||
"sha256": "2344976ded208cc091fc6bcffd65c663dcc7411dc06843cb7170b730c7e456a3",
|
||||
"bytes": 24275
|
||||
},
|
||||
{
|
||||
"path": "agents/memory-auditor.md",
|
||||
@@ -118,8 +118,8 @@
|
||||
},
|
||||
{
|
||||
"path": "agents/resize-auditor.md",
|
||||
"sha256": "05d362c8cefa9a82bd73bab8239dd4ac40709da27e705016965634691498a687",
|
||||
"bytes": 17740
|
||||
"sha256": "817ad9daf9a4f66b89d3b97675cae1c98b1da5ebecb1df23620e60b89fd24e8c",
|
||||
"bytes": 20515
|
||||
},
|
||||
{
|
||||
"path": "agents/screenshot-validator.md",
|
||||
@@ -173,8 +173,8 @@
|
||||
},
|
||||
{
|
||||
"path": "agents/swiftui-layout-auditor.md",
|
||||
"sha256": "9f26f8d840833ae86bf0db4b9bba3a0fbbefb6f8a94cabf8b4a922e8623ffcc0",
|
||||
"bytes": 13885
|
||||
"sha256": "de5836833d7e0ac1fee720c734539fb90399d9994049b1439d65e195bc9ec406",
|
||||
"bytes": 15939
|
||||
},
|
||||
{
|
||||
"path": "agents/swiftui-nav-auditor.md",
|
||||
@@ -233,8 +233,8 @@
|
||||
},
|
||||
{
|
||||
"path": "commands/axiom-ask.md",
|
||||
"sha256": "35cc570df2375fd4f3aab86d53d8dd0f477f675dd5b51103c8c70d18d22ff351",
|
||||
"bytes": 7791
|
||||
"sha256": "62377cf287600bd097d176d3830cc1cea2dd0452bec0ea60fe2a05b70c79c378",
|
||||
"bytes": 7803
|
||||
},
|
||||
{
|
||||
"path": "commands/axiom-audit.md",
|
||||
@@ -358,13 +358,13 @@
|
||||
},
|
||||
{
|
||||
"path": "scripts/user-prompt-submit.py",
|
||||
"sha256": "0fdba48bc3c05ac6e104ffeec8e540054362dffd250c930f3429102508bdf8f1",
|
||||
"bytes": 39327
|
||||
"sha256": "fdd123c20e276c795207bff863de92f292c7b930eb8d0922d3ddf35fcea077b6",
|
||||
"bytes": 46842
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-accessibility/SKILL.md",
|
||||
"sha256": "4e900826e6e979f6f6749204b99bc6e9d75dd11f963b3ee224fad0d5a17e2fe4",
|
||||
"bytes": 8835
|
||||
"sha256": "e485e00216002f3d86ec629ebf45490f8715e5672d5158eb534e1a2b7d1f3b32",
|
||||
"bytes": 8977
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-accessibility/skills/accessibility-diag.md",
|
||||
@@ -408,38 +408,38 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-adapters-diag.md",
|
||||
"sha256": "f71b52266e79c2aa19ad80858448438fd0a216e1438e2d329cce6d70abc19f3e",
|
||||
"bytes": 23323
|
||||
"sha256": "2faf36365d48d050c5f95cb145834bc10a4da5c788305b4b5d8ce6ac3eb1e086",
|
||||
"bytes": 23267
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-adapters-ref.md",
|
||||
"sha256": "ac3d3c2c29b3bb7afe2ae8a35e3e153f4836a7d2ede39941902f3521d753f615",
|
||||
"bytes": 32789
|
||||
"sha256": "38efdbb30f60e81de0259e473a86127b298272da1c5a4d435ee477019c302930",
|
||||
"bytes": 32734
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-adapters.md",
|
||||
"sha256": "d0dd3c657ede140b73a683df9eb62841480f10df39f13cb30174e40320e360c3",
|
||||
"bytes": 25992
|
||||
"sha256": "a73cd80bd0a16fac850d9a70a55ecb562ed0d1618c393bcdfe3e1e5b10e13f46",
|
||||
"bytes": 25936
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-diag.md",
|
||||
"sha256": "12d46250cacd1db4fb193de3e5f6640f64c7433ab0ef5556a368cf16f397e4e6",
|
||||
"bytes": 39591
|
||||
"sha256": "752f7b635c6742b20c9c90f9e42eb0f0aa00574f8ad1a392f0cccec7b4fe2041",
|
||||
"bytes": 39510
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-evaluations-diag.md",
|
||||
"sha256": "d669849d341cb5616f784783917c126d4747f6921a31056405ad34e117e066e6",
|
||||
"bytes": 23733
|
||||
"sha256": "96749974fd7929380e685129dd13fa9086179a947e54751f2135fcff15b2b836",
|
||||
"bytes": 23634
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-evaluations-ref.md",
|
||||
"sha256": "3ffdcf05fa50e5c5e1045187f223077e8af1043abd29a12a07d15fab54ef7aff",
|
||||
"bytes": 37833
|
||||
"sha256": "546d2962bfa724b83156df450cdce36f01c24ea685ff6adeacb8a07561ee1e3e",
|
||||
"bytes": 37736
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-evaluations.md",
|
||||
"sha256": "2c853626c4d5cd658208e0ad6cb3f75be4c85b7a35c556cb9755aad7a1e5cb85",
|
||||
"bytes": 51672
|
||||
"sha256": "0f04e8539f9029bf0d4346b0c2000c663d3e2c9e95b91451e3535171b37661de",
|
||||
"bytes": 51574
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-guardrails.md",
|
||||
@@ -448,13 +448,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models-ref.md",
|
||||
"sha256": "8dc607e47f28afad98685f833b6cd7ef546b68eca553d2d4729f891b08cafc4f",
|
||||
"bytes": 63983
|
||||
"sha256": "84a74b2653481af4ab6a0a68fcc7d1cf588eacb5504db899f7b3838bcc45952d",
|
||||
"bytes": 63928
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/foundation-models.md",
|
||||
"sha256": "2921f25c2b917ac276783817c115393538ebdfa6c4600b168d70c6907062e3d5",
|
||||
"bytes": 50591
|
||||
"sha256": "aba666d65b2823cb3347dff1bb418fb23474460f3e87a81cf9bb4ce3cf284126",
|
||||
"bytes": 50562
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-ai/skills/ios-ml.md",
|
||||
@@ -488,8 +488,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-build/skills/build-debugging.md",
|
||||
"sha256": "5fe2abf77411d061b634df712b373db9906aeecacddcccadf533c4d0ea08415e",
|
||||
"bytes": 14196
|
||||
"sha256": "3af51c88c74bbb364a98eec2a1d9dbe08370fb835f94231cc91bd23a303b1d55",
|
||||
"bytes": 15753
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-build/skills/build-performance.md",
|
||||
@@ -533,13 +533,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-concurrency/skills/swift-concurrency-ref.md",
|
||||
"sha256": "46a93e5d9390cf242061627dd699ac06fd9315ee5321ea96453adc0428a99da0",
|
||||
"bytes": 56387
|
||||
"sha256": "dfc4e482ae4c78543097ebb36248e5a57dbda1fa0f0111a09b276ab323df1da7",
|
||||
"bytes": 56449
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-concurrency/skills/swift-concurrency.md",
|
||||
"sha256": "e040b44445b40eb5de3c61de409096dd4480890130ebe0e3d7cb806ee282572a",
|
||||
"bytes": 54645
|
||||
"sha256": "c9104c9443db91921c523c97967593760f8c242fe1e8ef7500ce40daf577d541",
|
||||
"bytes": 54465
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-concurrency/skills/synchronization.md",
|
||||
@@ -553,8 +553,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/cloud-sync-diag.md",
|
||||
"sha256": "5fb8ecf249d0baa2442009fa16d8def442a3b10b5646d321b9f9f0a7d8774dec",
|
||||
"bytes": 22122
|
||||
"sha256": "441de1405c5b3b86a79ec3810892a489a6b3c74a78f2107edcc365d32fe29cec",
|
||||
"bytes": 22060
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/cloud-sync.md",
|
||||
@@ -563,8 +563,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/cloudkit-ref.md",
|
||||
"sha256": "8f113ca56e310599f444669fc9765fca324fa73c37eef6ba8ec1c8cdc9f2e5e3",
|
||||
"bytes": 21146
|
||||
"sha256": "7599b3b8e8c10018f8cdad61e973f956ce43e538ca79b2fde29bd1df99b290c6",
|
||||
"bytes": 21091
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/codable.md",
|
||||
@@ -573,8 +573,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/core-data-diag.md",
|
||||
"sha256": "ad454b0b16f75c3773d5d84cb9c7a54e0859d6e0a72841d604d997644c24ce23",
|
||||
"bytes": 36849
|
||||
"sha256": "abf245355ef700f0d18338541fa0b460d2d59f7e1d8b124e880f1caf1306594d",
|
||||
"bytes": 36773
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/core-data.md",
|
||||
@@ -583,8 +583,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/database-migration.md",
|
||||
"sha256": "eb44475bf5bfb3a4529e8db6bd6c46e566d84f3721f71901883f7ecfd9846341",
|
||||
"bytes": 15169
|
||||
"sha256": "a08c8abac81b3797e0f7bfbfc1b3db69a5aa3674f7442cd236c6219a415a8014",
|
||||
"bytes": 15076
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/grdb-app-groups.md",
|
||||
@@ -598,18 +598,18 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/grdb.md",
|
||||
"sha256": "4656717580541812f8ec2549e15997d2f9f4713c9254a98710b309dd0d5207e5",
|
||||
"bytes": 24569
|
||||
"sha256": "fa4f867b223ea39a438df543750525bfbbbad09999c1a543964e22708795500f",
|
||||
"bytes": 24532
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/icloud-drive-ref.md",
|
||||
"sha256": "f7f63b248461f88b7785e18edfb1791b934b4787ec242ad6f270e450e61f259f",
|
||||
"bytes": 12230
|
||||
"sha256": "c98700b587e98c5ad6024e519498381bfb71a5983fb170bc39ab6daa4a6593b5",
|
||||
"bytes": 12175
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/realm-migration-ref.md",
|
||||
"sha256": "018600c615a9f621f71f563c8750dc8f495e2cc6ecbf153a33052c66cae4b743",
|
||||
"bytes": 22664
|
||||
"sha256": "d5cecf3101972e26a5858a0e86d072fc9d22acee8a277a74cafe8122d637aca5",
|
||||
"bytes": 22595
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/sql-json-ref.md",
|
||||
@@ -623,48 +623,48 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/sqlitedata-migration.md",
|
||||
"sha256": "6953c2b8ac0f2c8e588e740506345627814febab58496291b5ec412b112ada4b",
|
||||
"bytes": 8733
|
||||
"sha256": "560146d9f3654803a12cea4d03b7f3d00290a91ecee776bc58881cc4aa6f0274",
|
||||
"bytes": 8690
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/sqlitedata-ref.md",
|
||||
"sha256": "b487dfc49b34ce31a108e275042f5a26544f73a6891b4c03c7cbe0bf67e1c2b3",
|
||||
"bytes": 32352
|
||||
"sha256": "c1f0178a0ff4c4131d93dc1c7af37cea27615cbab99f64e35ba73004b29628e2",
|
||||
"bytes": 32315
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/sqlitedata.md",
|
||||
"sha256": "00c7e8852622de0418520de2b65a4461ee31893afa621f3df34ab9818242efa7",
|
||||
"bytes": 31531
|
||||
"sha256": "0c2ad86c50f0c029e31cf9672891a8275b5bd94451b314d86cbd7622eefc43b2",
|
||||
"bytes": 31494
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/storage-diag.md",
|
||||
"sha256": "15a892c680fcdcaad2cb41cd5b7417330fe71c9178fb14158a13650a79babd3e",
|
||||
"bytes": 14494
|
||||
"sha256": "03fbcf782c937bff10cc4ec1b1990a8d9cb9102f75083cc06d8189c0470889ee",
|
||||
"bytes": 14432
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/storage-management-ref.md",
|
||||
"sha256": "32b5efeb6b5e9c6b998c22e2db88213340ec740a07be3e588000f9054fb3d40e",
|
||||
"bytes": 18677
|
||||
"sha256": "73146cc1ab24f02f18cb6a42a6785dcaee6da2bee06c1d4a7ad9ce73d5beba69",
|
||||
"bytes": 18622
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/storage.md",
|
||||
"sha256": "74fa79516b1a89a6f89462b5ef67eab640510d886f36b293a5171035068bb9eb",
|
||||
"bytes": 16963
|
||||
"sha256": "b860aedff37d2518f6d56fbbced9069875aededfc02579ca1c05188a9e11fac7",
|
||||
"bytes": 16907
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/swift-sharing.md",
|
||||
"sha256": "ca61cc980347ef5badb76340efde69cd2ae0d6a1495055cdeb17937fb7498eb7",
|
||||
"bytes": 12159
|
||||
"sha256": "44c92a619be8fa92d9345a1ffab9038a8f754aff739e59e666c78460a88e4bba",
|
||||
"bytes": 12122
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/swiftdata-migration-diag.md",
|
||||
"sha256": "fa83a171b626700fa7a0351a509d39f6bbfe9e22a9c7a7144d2c7dde1d1bbb53",
|
||||
"bytes": 19348
|
||||
"sha256": "db5bc60f64fb4212b57714ec890a06173af7d64ad523abc03d2562a78e36844b",
|
||||
"bytes": 19277
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/swiftdata-migration.md",
|
||||
"sha256": "661350dbe03d465cf57734878ebe92fd3935e18d728171eab67b533b9fd68746",
|
||||
"bytes": 29509
|
||||
"sha256": "1eeef72720c7db24d127d06c30077c3f17314d94d970425fa0432fb1dd5e82e1",
|
||||
"bytes": 29486
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-data/skills/swiftdata.md",
|
||||
@@ -673,43 +673,43 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/SKILL.md",
|
||||
"sha256": "9929a7b95c78525b2ab13fac998d953ff6bc9da31fd9f6b9d2ac1aa8667978a5",
|
||||
"bytes": 10904
|
||||
"sha256": "bc2ae3c1286a5163c8f3b38e0a5a5f6e7a3908cab8de8c371333cfb3e9c246fc",
|
||||
"bytes": 11389
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/app-composition.md",
|
||||
"sha256": "ae02149fab21cce7d1aa9056169554207363a1a7f25d655538173a2a8850fba1",
|
||||
"bytes": 46753
|
||||
"sha256": "f02dedd491cb0fd6d8c12cc198a2ec18ab357e331e4ac6b8da0a5bbe7d0033e5",
|
||||
"bytes": 47055
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/hig-ref.md",
|
||||
"sha256": "1fe982cf599d25a7081a7fab9668b0d12e935b2f794367287ee1986e3049af35",
|
||||
"bytes": 39346
|
||||
"sha256": "cf529906132acfe3949756dee27a9b40fc0378299383795095966852a0e810d4",
|
||||
"bytes": 39205
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/hig.md",
|
||||
"sha256": "48fb2f2675e11cd642b43e96925a209e8d3d9ff53f1e8ac7d0c5e62d135f4757",
|
||||
"bytes": 19669
|
||||
"sha256": "22e0f861e19fe6594e8ee11169dc3ad1f013a188416293e9b32de8fe9232a2bb",
|
||||
"bytes": 19527
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/liquid-glass-ref.md",
|
||||
"sha256": "5ee740e74128d146dbb0add59857b355af43dceaed77cf794a10d853c8a1a651",
|
||||
"bytes": 29049
|
||||
"sha256": "51fed0dde6849dbc45b3ff16c34519508f6cdcaa2a6331fae3b6166399279b0c",
|
||||
"bytes": 29326
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/liquid-glass.md",
|
||||
"sha256": "c229e54f443be0146791261812b8d751480974f577d9635307a29634999c0939",
|
||||
"bytes": 23000
|
||||
"sha256": "60414732687aa89e4cf34184f0878ee9e219986e910bf9dcf5ad44b102874c6c",
|
||||
"bytes": 24364
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/sf-symbols-ref.md",
|
||||
"sha256": "10570fb52e5af6e1c2f0fe51a30ec9bc100f7d9e88dbd0e6c0b6053bd1bac190",
|
||||
"bytes": 32361
|
||||
"sha256": "9b1e2b74ca0d9e150ae2ef102857c220204737bccd5e2b59908e5dd725c04b39",
|
||||
"bytes": 32281
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/sf-symbols.md",
|
||||
"sha256": "adec3a5f07cf1bbc31aa2eba50dd225f413d7cb17692fbb823a0fc148856f1a1",
|
||||
"bytes": 23591
|
||||
"sha256": "83f3305de5e550fd652d116f01eed7074be6f4250acb88472f127cf9f600737d",
|
||||
"bytes": 23511
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-design/skills/typography-ref.md",
|
||||
@@ -743,8 +743,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/SKILL.md",
|
||||
"sha256": "b96806de209838a02de2c9eaba06ca3ef70bc24c5ae1bb848f3ddbefa57afeaf",
|
||||
"bytes": 12015
|
||||
"sha256": "ef1d1da0d81398f1df812af39aa7bafaa6c400966076afa6eeb07bfdfd583c69",
|
||||
"bytes": 12004
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/display-performance.md",
|
||||
@@ -758,18 +758,18 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/metal-migration-diag.md",
|
||||
"sha256": "ee6acead5fe8547d9a9765238688fa997c83d2bc4e10fe517628e7c777b58b71",
|
||||
"bytes": 20352
|
||||
"sha256": "0ceb0d0f897165370430f3fabb508f94b7a62ce64afff7317c0f875adeb123ce",
|
||||
"bytes": 20271
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/metal-migration-ref.md",
|
||||
"sha256": "05f70fef3870670dd9ae66465d76d2c19f84308b57307e1596fdbba72b81c3dc",
|
||||
"bytes": 25398
|
||||
"sha256": "8bd6456ff7950e0c3c49ecaedf05a5c0d78e7a914317579dbde896f5db3cd6b7",
|
||||
"bytes": 27208
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/metal-migration.md",
|
||||
"sha256": "431f5e2d89dbe50214ec0a19a52d6366c3045a8ffd16bee516c21c28c128fd58",
|
||||
"bytes": 17217
|
||||
"sha256": "f25193cba744fc69bd69e724e98b8718a4dc2d16f84d6d52ce5f109fd3e141b1",
|
||||
"bytes": 17134
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/realitykit-diag.md",
|
||||
@@ -778,8 +778,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/realitykit-ref.md",
|
||||
"sha256": "4cb20d598486967c2b8479f05d1b5552b7fcdfe700ae3e968595a015784a9cc4",
|
||||
"bytes": 42167
|
||||
"sha256": "5d647bfbef03418a340937be8df2db284d3a8c10b408a08e03c2e904e952fb93",
|
||||
"bytes": 41959
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-graphics/skills/realitykit.md",
|
||||
@@ -823,8 +823,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-health/skills/fundamentals.md",
|
||||
"sha256": "14dd9cbd5aa9a04e00ba3e7399dbe63a30d70f0cdc51b78a5619b0c9543583ca",
|
||||
"bytes": 11572
|
||||
"sha256": "e189aa28e676397f4bb4c8e599b10047da006546bcdf6be659c8518d1830214f",
|
||||
"bytes": 11881
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-health/skills/queries.md",
|
||||
@@ -838,8 +838,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-health/skills/wellbeing-and-medications.md",
|
||||
"sha256": "5b4859293fe6c4b0aae6c4f5e91ae54639280546cbd0f33a513a32992b728765",
|
||||
"bytes": 13318
|
||||
"sha256": "9c387f9d39aaebe296cdf7d39e0b4f41d20ebfa8aee86ac5bdde737f51d7c30c",
|
||||
"bytes": 13276
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-health/skills/workoutkit.md",
|
||||
@@ -888,28 +888,28 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/background-assets-ref.md",
|
||||
"sha256": "42d6832cb92ddd0683b031df7df634514479cac50df682359dab01f4da154463",
|
||||
"bytes": 39624
|
||||
"sha256": "0af4f94ab65c480cddaefbaaf687c2a8e3b49078a0e35e45c2122a8b708d54ac",
|
||||
"bytes": 45854
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/background-assets.md",
|
||||
"sha256": "28e982e2647220a201315364006cc980562048ca89b03021b421cc20bdc08ed2",
|
||||
"bytes": 26055
|
||||
"sha256": "350a98c3a254adf2e8e41f8919c1bd893720dde5123e75b365fae02a4e0b7b4c",
|
||||
"bytes": 26326
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/background-processing-diag.md",
|
||||
"sha256": "b34ca735de1df2ac5ea7785c1388815b35aa85762c29bc13be234312adbecd95",
|
||||
"bytes": 15383
|
||||
"sha256": "c1ce5df28842e2605e3b95886f5e744a030d5c37b8f8d31632a8a4ea30a9de0b",
|
||||
"bytes": 15354
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/background-processing-ref.md",
|
||||
"sha256": "763bcec44a452c67634d518bb88befe8042567fa64f703392fda0c082ec1a842",
|
||||
"bytes": 20804
|
||||
"sha256": "6c60c803c4ebf070bb879f0ceea6b78a260eba571651e565d8b16454e97d5b24",
|
||||
"bytes": 20775
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/background-processing.md",
|
||||
"sha256": "b61e9554980f3b62e2179a4652c31cf8091df3c818b3cf8c2d8ed7139cfc7254",
|
||||
"bytes": 33151
|
||||
"sha256": "c828753f5ba75e8fdab66c3204787ad76107904fe4927e12f05cc7d8fcaf9943",
|
||||
"bytes": 33068
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/callkit-livecommunicationkit-ref.md",
|
||||
@@ -948,8 +948,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/extensions-widgets-ref.md",
|
||||
"sha256": "98081ddfe3d41e506509e6c31d040438a5627bfe72c1a2dcd45afd00b123adcf",
|
||||
"bytes": 37193
|
||||
"sha256": "50522886dc1899e7a584b206ca3d3f577082c942188cb28fa30fa76841d03084",
|
||||
"bytes": 37173
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/extensions-widgets.md",
|
||||
@@ -1018,8 +1018,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/storekit-ref.md",
|
||||
"sha256": "16ed62ccd1351d78cd20d0b384dc103bdca6fa121a3912425c65646a5635555a",
|
||||
"bytes": 51193
|
||||
"sha256": "ad02dccf184f0842ba918a9c53b03ebc30a1c0360b989dd311dfbbeaeca4692f",
|
||||
"bytes": 51208
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-integration/skills/timer-patterns-ref.md",
|
||||
@@ -1073,8 +1073,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-macos/SKILL.md",
|
||||
"sha256": "a7c2a92517cce7066d64cd0708545295586880f05e3830fe1b23988bc1730681",
|
||||
"bytes": 4724
|
||||
"sha256": "1ee8f5ba4ff0ed60162f2e4413c5c0f74b04e37451e568ea4d7b2337fc30f81a",
|
||||
"bytes": 4895
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-macos/skills/appkit-interop.md",
|
||||
@@ -1103,18 +1103,18 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-macos/skills/sandbox-and-file-access.md",
|
||||
"sha256": "21b448431f5086f928bc231a2b35e0ea43757d4fd12532fd4d5ebfca0d694296",
|
||||
"bytes": 19499
|
||||
"sha256": "f560cea2d1a6e42875a282a39e72f46e3fda862e654825ba19bcb133846c7d98",
|
||||
"bytes": 20152
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-macos/skills/screencapturekit-ref.md",
|
||||
"sha256": "b2af48e80fbbe4209fa20825c31d450f4840468ec9139d5b169705fc6760f731",
|
||||
"bytes": 8108
|
||||
"sha256": "16760cdef6f6fbc6aa10a74f30fbc235e4e829f4b3e3c6d3179db5326772a0cd",
|
||||
"bytes": 14790
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-macos/skills/screencapturekit.md",
|
||||
"sha256": "5e75e8b85c4af282569b509441e5f53cacb6b60bcc3344b67002a693e9826707",
|
||||
"bytes": 10342
|
||||
"sha256": "8ac26cb10af1c6b8b910b9bc0e03a604278064242749307773d67cd2f992fb1d",
|
||||
"bytes": 11004
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-macos/skills/settings.md",
|
||||
@@ -1133,13 +1133,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/SKILL.md",
|
||||
"sha256": "305794ff464a1716a46a3307ac4a2e40c6e02c97a1b397d12ee21435fdd4f60a",
|
||||
"bytes": 16127
|
||||
"sha256": "8606362667bf07662fcd832fa1df09646078e1e51165f0d5bbe3b8b1567a728f",
|
||||
"bytes": 16951
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/avfoundation-ref.md",
|
||||
"sha256": "7cf1e48008b223bf439d20189cbd15bf68c6e1b4dadb11a40f79ee45d5c67fab",
|
||||
"bytes": 15945
|
||||
"sha256": "58db62345162aba46495d134732d729abe45dd3a62fdfca2875dda855b38743b",
|
||||
"bytes": 23701
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/avfoundation-video-ref.md",
|
||||
@@ -1153,13 +1153,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/camera-capture-ref.md",
|
||||
"sha256": "21158acb5ccce6f2d4c3e5d244e1982aeeef0caf60ab1ab3a5d69e6fe8f6ab07",
|
||||
"bytes": 34904
|
||||
"sha256": "5fbb349f326fb1027a8c9eb9eb22c76b8478152022c9bd86e24676d67f8b8d37",
|
||||
"bytes": 48689
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/camera-capture.md",
|
||||
"sha256": "c44a6185c1c00c99410d62c37429c9ee44c11d4a16a1b743bd48e29698453b20",
|
||||
"bytes": 34565
|
||||
"sha256": "bd84a77515bf52d8159e4d715c82ec6f25b36610a6fdfd50c8b4b4f60d04ca37",
|
||||
"bytes": 39357
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/carplay-hig.md",
|
||||
@@ -1213,13 +1213,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/now-playing.md",
|
||||
"sha256": "75111931d624dae20c0ec09d1afb62ff12bb215bf2981caab6ab39b5c867ad2f",
|
||||
"bytes": 52593
|
||||
"sha256": "9e0dfc59fdfcfce7396237164d4c05d9467de47c14fb4294dc3988188d3d0986",
|
||||
"bytes": 52403
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/photo-library-ref.md",
|
||||
"sha256": "bb07221116c0a4c062d57976e1d848a5bcfda212ea18021d6c21ee45a482dafc",
|
||||
"bytes": 26359
|
||||
"sha256": "223f0d4e40eb0607393c3e4ce9ada830fb3a0b13c1395dbf8dbf4ccb6e127163",
|
||||
"bytes": 38122
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/photo-library.md",
|
||||
@@ -1228,8 +1228,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/screen-capture.md",
|
||||
"sha256": "0f353b3658f36d59a2b469111805010a5b9c96b6063fb79ef854c0fd91a3af32",
|
||||
"bytes": 6147
|
||||
"sha256": "ac62a1cc78a66abb904b479d33c2a3b136094cc652cb1c3422c5f8d92f6f6a6e",
|
||||
"bytes": 6471
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/shareplay-playback.md",
|
||||
@@ -1248,8 +1248,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-media/skills/system-media-routing.md",
|
||||
"sha256": "fac0298bbfa0e4f49883a13a3b59726c13b7bca8f4dc7f2b67657df1f62e2682",
|
||||
"bytes": 20598
|
||||
"sha256": "3efa46a9b4b5b9ad18ab4158e3a977266ce907d3667bdf4d5f547fb33890e7e3",
|
||||
"bytes": 20556
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-networking/SKILL.md",
|
||||
@@ -1258,18 +1258,18 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-networking/skills/network-framework-ref.md",
|
||||
"sha256": "57093b8522c63c487032ce4aee46a3c9cac27ea61d88acdf5d69645ef72295a9",
|
||||
"bytes": 45862
|
||||
"sha256": "386a99aeea011df4ccc4fbc4c467f1478d69a9d45a1713d14d31761d410a7f3f",
|
||||
"bytes": 45767
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-networking/skills/networking-diag.md",
|
||||
"sha256": "483a230240f94f42b4951f46fdfdfca17d77da47b72589a6ee64f25f6b926d7e",
|
||||
"bytes": 45246
|
||||
"sha256": "5cdacae6871b8b833915d533c7c264ab67ff9daa2a98b7481d7e7f4a0af773c6",
|
||||
"bytes": 45080
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-networking/skills/networking-discipline.md",
|
||||
"sha256": "2dba32a803a1fbfefded9442182f98bb7a790ecc59fe42f5516ac92cd530204a",
|
||||
"bytes": 38841
|
||||
"sha256": "3fa53d4fb085c8dc2fed83b23faf53a89ff3f80713abcfe0478268c563ce2f6c",
|
||||
"bytes": 38660
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-networking/skills/networking-legacy.md",
|
||||
@@ -1358,13 +1358,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/energy-diag.md",
|
||||
"sha256": "1a6cc57c225ad3f1a4bc828ad4e8e9af51eb7dcc3c348ad11f4c00b6f341932d",
|
||||
"bytes": 15407
|
||||
"sha256": "a6d70350c9453801b7b9a3fbcbd8b40d46b3023a6c0b03eac9adf6fa7963f750",
|
||||
"bytes": 15373
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/energy-ref.md",
|
||||
"sha256": "1a2279f6631d13eb50027be86361b17b725b84ae9f34e5134006a28924cdef38",
|
||||
"bytes": 28921
|
||||
"sha256": "d6900574c787b75615ce97cc954bea0daa2d96de2233088719526eb782142330",
|
||||
"bytes": 28887
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/energy.md",
|
||||
@@ -1383,18 +1383,18 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/metrickit-ref.md",
|
||||
"sha256": "de85d96a8c2ef481258452bc1ecc9d24696721c43487173dacb90d3106b285c8",
|
||||
"bytes": 37073
|
||||
"sha256": "be4511a368b9018d3b99f2455ae61022a6c1ba811f4ed9acacb358b2121882f0",
|
||||
"bytes": 37923
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/objc-block-retain-cycles.md",
|
||||
"sha256": "ecdbe01f10873c328b0f4a83eacf02a935f293dcdb1aa83194f41a37d945f87f",
|
||||
"bytes": 22971
|
||||
"sha256": "3290e13e3fad7cd86471b98d62b5b2d2e0dd127de99f827fdf8957d953932816",
|
||||
"bytes": 22895
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/performance-profiling.md",
|
||||
"sha256": "326dec644c2b1d6811ba08ea37e77fdb072fd3bdb07e65fce7016e93fd28642c",
|
||||
"bytes": 38923
|
||||
"sha256": "201bbc75c9f653f01af8ae600be7ff3f06f7b2c01964a3c90b5d71e05f1817dd",
|
||||
"bytes": 38886
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-performance/skills/swift-performance.md",
|
||||
@@ -1453,8 +1453,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-security/skills/file-protection-ref.md",
|
||||
"sha256": "40eefb2cda76873b1d3d80e4b294a79ecc46649243f748e78461c4cef73e35b9",
|
||||
"bytes": 15559
|
||||
"sha256": "459bfa81e268e8e85800755795f9a73a41f3eb9a2aed470f9dd28e506dceae7a",
|
||||
"bytes": 15504
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-security/skills/keychain-diag.md",
|
||||
@@ -1548,8 +1548,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swift/skills/ownership-conventions.md",
|
||||
"sha256": "f28e4373591c778b006f086b9a5cd064cd55c0786c638947ee48471484441f58",
|
||||
"bytes": 18397
|
||||
"sha256": "eb0fe8af26bac487b6e4f5fed3b61b902706bad958a8240165e8f70c104e08db",
|
||||
"bytes": 18383
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swift/skills/swift-modern.md",
|
||||
@@ -1568,13 +1568,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/SKILL.md",
|
||||
"sha256": "6394b6676c654cfbd795223db1797fe40ff33b13b670ab7b79f3e4b7a2e23f7c",
|
||||
"bytes": 10088
|
||||
"sha256": "a0425326a83a8f851a9821fc8a88244555b278a17c8eb771283063c2e8cb8647",
|
||||
"bytes": 10748
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/26-ref.md",
|
||||
"sha256": "3dd10abc5371c876db8b5648f4fbea2402594574091a7cca2499bc784f2e92b3",
|
||||
"bytes": 51771
|
||||
"sha256": "6674e807bc88c5e74f10706a9063d26e2c626080697ce0877d3f84aa3bf4258a",
|
||||
"bytes": 51982
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/animation-ref.md",
|
||||
@@ -1583,8 +1583,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/architecture.md",
|
||||
"sha256": "3c817f25ab556c41e6e2923900bbfd4b1961bdb5697cb001a570936977bc8e99",
|
||||
"bytes": 65114
|
||||
"sha256": "1efe7483bb8c7e130a68bbe087d6b2eaea143a30dc2d8cc5f52550e2127cffc8",
|
||||
"bytes": 64992
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/containers-ref.md",
|
||||
@@ -1611,6 +1611,11 @@
|
||||
"sha256": "1fe48a3abfceb61255808a5e5d37fc95a8d5d9c8e49992ded305862431712097",
|
||||
"bytes": 9706
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/iphone-duo.md",
|
||||
"sha256": "57e151e4d610b5c5881ca7e5c25f8d2c2abc9f6a20229168d259b50705d71aba",
|
||||
"bytes": 26936
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/layout-ref.md",
|
||||
"sha256": "897d5acb9af2e2d21f9785cd496b73d807d7e1e5fadfda07c29e04c3b178634c",
|
||||
@@ -1618,28 +1623,28 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/layout.md",
|
||||
"sha256": "8b02208c4d187fdcc8bd620a98003aadd8e55812c15eb0d12affce77253f802b",
|
||||
"bytes": 17729
|
||||
"sha256": "ab067808a14a91aedbe7d90dbff731e663e114c9b1d5ad024e3f9361715d3eb1",
|
||||
"bytes": 18172
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/nav-diag.md",
|
||||
"sha256": "203a0ae48eaaf9b60f98514ff754bdee517c6b5335b7caf9d3aeae2412930a69",
|
||||
"bytes": 41915
|
||||
"sha256": "1f88ff4f6f6834529efae5f605be57bb0a0b4a6bcf8a221dfc9613eae7278b30",
|
||||
"bytes": 41767
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/nav-ref.md",
|
||||
"sha256": "80e4f358a046f55fd2385d254366df3fb8b123d3ab1bae9d898f15966df2a783",
|
||||
"bytes": 34890
|
||||
"sha256": "ae27f3a361ea71125787a32fec1eb09393cd594d1a7d3a8e5a93c9bfe7341915",
|
||||
"bytes": 34759
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/nav.md",
|
||||
"sha256": "fb495c7ce3ae0108f2420eee768aeffca6d475474586a84157599dd83d657239",
|
||||
"bytes": 24326
|
||||
"sha256": "6266fa82a341bb601849e39e86d2a2041b6a92075c9cd4246fa51e9761069ad9",
|
||||
"bytes": 24265
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/presentations.md",
|
||||
"sha256": "f3a2ff7b49e388cddcdc9c448a596ac331ce604af03898e7d0e1247aee114fc5",
|
||||
"bytes": 5682
|
||||
"sha256": "e6974e6bfdafca8593da62b5ac66256b6a9bd43b8e95b3827fa5d2089c4a99b7",
|
||||
"bytes": 5988
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/previews-ref.md",
|
||||
@@ -1653,28 +1658,28 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/search-ref.md",
|
||||
"sha256": "8239fef6e41a80a391b2f9e99778fa31743678280d60c0db0772f5f4906ab013",
|
||||
"bytes": 19918
|
||||
"sha256": "3fb31aedba51a69495ca91d7b7aa4a078a9803ab449be076cf9321231c2f3306",
|
||||
"bytes": 19829
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/swiftui-performance.md",
|
||||
"sha256": "44a00e3c6e713ac64d0bf37aaa4cf6984c6634e8fc684da1d974efe3644a7dc1",
|
||||
"bytes": 36042
|
||||
"sha256": "806c166a0524cfd8ee86db86a70309c87b63decb19f05d3efb4276fa3ade7ca9",
|
||||
"bytes": 35981
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-swiftui/skills/toolbars.md",
|
||||
"sha256": "6dc3087280b87780d705e6415d901fbdac9086f2d9f93094c010fbc8a8d1609f",
|
||||
"bytes": 28503
|
||||
"sha256": "4b7af155c3ecfb83b6b18eb302afd310450584ba745aacdb38daa5df6ac8a613",
|
||||
"bytes": 34182
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-testing/SKILL.md",
|
||||
"sha256": "dc729b40a7e9bcd28fb787668eeb2d2c76be676e558ce0c3c0cce130d01954d7",
|
||||
"bytes": 8914
|
||||
"sha256": "1deb0b989ae3840b5bc5394ea386fe547038fcc8718782ecba6c8e56720f274b",
|
||||
"bytes": 9024
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-testing/skills/swift-testing.md",
|
||||
"sha256": "aefecab4e655932f13081dde7bdcf0ee5591dd7bc6457d22d5f799836600dcad",
|
||||
"bytes": 25147
|
||||
"sha256": "0c161d1a4617f389c7865c1c3b3049f09c0ec33fc93eb5fa3ccc89817d724cfd",
|
||||
"bytes": 25104
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-testing/skills/testing-async.md",
|
||||
@@ -1688,8 +1693,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-testing/skills/ui-testing.md",
|
||||
"sha256": "dbf2dd8ec146a37b349b882f5939741207d528382adfbda4734175be8c5c5f94",
|
||||
"bytes": 50016
|
||||
"sha256": "730e8d501a6ef3ef2f81bcbed8852f77abc41e74317fe85b3daf38444fe43713",
|
||||
"bytes": 51684
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-testing/skills/xctest-automation.md",
|
||||
@@ -1733,18 +1738,18 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/SKILL.md",
|
||||
"sha256": "5b17118b2b5629a9df6a939ec007740b141656c8c04951d6020a8c65e9a7681d",
|
||||
"bytes": 9218
|
||||
"sha256": "baf4c1672932e1a755c7b0e73cf37495850cd77e10661eaf66ec57565bc226f8",
|
||||
"bytes": 9843
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/adaptive-layout.md",
|
||||
"sha256": "b81848287efd94de12313034771c53f20ce0fa1d566bbb717090d358977be99e",
|
||||
"bytes": 5772
|
||||
"sha256": "9586ef3b15a496773e6ea3454c2271a4e9d4d223edde08442ac6df420a2b8818",
|
||||
"bytes": 6039
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/auto-layout-debugging.md",
|
||||
"sha256": "c840759b8870336c4e098552aa0213bb40511667ef5875ba859b8faa7142e4cf",
|
||||
"bytes": 19007
|
||||
"sha256": "eed27e3f0159b9cd1fa88fc2ff29e5329077bf697ec1a217010e71f57fa65ff2",
|
||||
"bytes": 18984
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/combine-patterns.md",
|
||||
@@ -1763,13 +1768,13 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/textkit-ref.md",
|
||||
"sha256": "f8367d8b7161ee570e363a9d705ce5318414631cedaebb74bb443e6a0e431521",
|
||||
"bytes": 29887
|
||||
"sha256": "6de652250700a47116bfc75a130804629678e18cf673261b671b2a947af68de7",
|
||||
"bytes": 34205
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/uikit-animation-debugging.md",
|
||||
"sha256": "4989c2ca6d4f2576e3df1f85488eff3611f3136429a446abe53e38a171ad2e98",
|
||||
"bytes": 18171
|
||||
"sha256": "a11069698f3556ceae52cb71fd25ff488228dbbb30ca816b537c4084561d776b",
|
||||
"bytes": 18095
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/uikit-bridging.md",
|
||||
@@ -1778,8 +1783,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-uikit/skills/uikit-modernization.md",
|
||||
"sha256": "80d8dcfc34fad0c36dfce681309e0482f03dbc4ddfe2a8c771f7867f4d971bf0",
|
||||
"bytes": 18435
|
||||
"sha256": "c060f73dc5a4cb6e4cc627fe85226a25a26d4451614b9b2bf8a8b1248c5f1711",
|
||||
"bytes": 26344
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-vision/SKILL.md",
|
||||
@@ -1848,8 +1853,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-xcode-mcp/SKILL.md",
|
||||
"sha256": "55672d39296a045ea9f005f329f83fcad0741721ede953775f8dbda57f66b7f9",
|
||||
"bytes": 8510
|
||||
"sha256": "468ae94ab6c6822164769646e386453744037b5c71cc4e89f885b2c2700b87dc",
|
||||
"bytes": 8513
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-xcode-mcp/skills/axe-ref.md",
|
||||
@@ -1858,23 +1863,23 @@
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-xcode-mcp/skills/xcode-mcp-ref.md",
|
||||
"sha256": "00565592c72121b3d52aeca92c9cf0a015753e596530e9baa02a7f80e7c94723",
|
||||
"bytes": 32094
|
||||
"sha256": "3ef381c1936fe2071def5c6efe9f98491321b4167e737da2a7c80435a90b3b69",
|
||||
"bytes": 32262
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-xcode-mcp/skills/xcode-mcp-setup.md",
|
||||
"sha256": "680deac8ab6a6dd60bdbe9857656f2386a60500c6b152dd80a73296019c98e55",
|
||||
"bytes": 16596
|
||||
"sha256": "ce88b8549cdde13f9112334f00a07bceb7d50210754f0f5e4149202563234660",
|
||||
"bytes": 16649
|
||||
},
|
||||
{
|
||||
"path": "skills/axiom-xcode-mcp/skills/xcode-mcp-tools.md",
|
||||
"sha256": "13788b3cff8382acb251bbe6659f215f9238f887edf9fc536cf2678210385933",
|
||||
"bytes": 13678
|
||||
"sha256": "51658752a0ac6b2a5885345b7d0439b4187c0297c8b4f92f578a1d46dfbf3a11",
|
||||
"bytes": 13931
|
||||
}
|
||||
],
|
||||
"totals": {
|
||||
"files": 374,
|
||||
"bytes": 7237822
|
||||
"files": 375,
|
||||
"bytes": 7361762
|
||||
},
|
||||
"excludedMirrors": 30,
|
||||
"classes": {
|
||||
|
||||
+85
-9
@@ -123,13 +123,42 @@ _explicit_invocation = re.search(r'\b(use|using|invoke|run|load|try)\s+(the\s+)?
|
||||
axiom_meta = bool(_axiom_meta_subject) and not _explicit_invocation
|
||||
|
||||
# Build/environment (highest priority)
|
||||
if not non_ios and re.search(r'build (fail|error|broken)|xcodebuild|simulator (crash|hang|won.t|not )|pod (install|update)|spm |swift package|linker (error|command)|module.{0,5}not found|derived data|code sign|provisioning|xcworkspace|xcodeproj|xcode (error|crash|hang|won.t)|build time|compile (error|slow|time)|lldb\b|breakpoint.{0,10}(set|conditional|symbolic)|thread\s*backtrace|\bpo\b.{0,10}(vs|variable|expression)|transport error|could not be established|\bcoredevice\b|dvtenablecoredevice|deploy(ing|ed)?.{0,30}(to\s+)?(device|watch|phone|ipad|simulator|hardware|real\s+device|physical\s+device)|connect.{0,10}(to.{0,10})?(watch|device|phone|ipad|simulator)|device.{0,10}(not.{0,10}(connect|found|recogn|appear)|won.t.{0,10}(connect|appear|show))|cannot find symbol|cannot find.{0,15}in scope|use of unresolved identifier|undefined (symbol|reference)|works? (fine )?(in|on) (the )?simulator.{0,40}(fail|crash|broken|wrong|black|empty|hang).{0,15}(on|in).{0,10}(real |physical )?device|(crash|fail|broken|wrong|black|empty|hang)\w*\s+only\s+on\s+.{0,15}device|only\s+(crash|fail|broken|hang)\w*\s+(on|in)\s+.{0,15}device|(real|physical)\s*device[- ]only|device[- ]only.{0,15}(crash|fail|broken)|after .{0,30}(updating|upgrading|installing) xcode', prompt_lower):
|
||||
if not non_ios and re.search(r'build (fail|error|broken)|xcodebuild|simulator (crash|hang|won.t|not )|pod (install|update)|spm |swift package|linker (error|command)|module.{0,5}not found|derived data|ld_classic|\bld64\b|classic linker|code sign|provisioning|xcworkspace|xcodeproj|xcode (error|crash|hang|won.t)|build time|compile (error|slow|time)|lldb\b|breakpoint.{0,10}(set|conditional|symbolic)|thread\s*backtrace|\bpo\b.{0,10}(vs|variable|expression)|transport error|could not be established|\bcoredevice\b|dvtenablecoredevice|deploy(ing|ed)?.{0,30}(to\s+)?(device|watch|phone|ipad|simulator|hardware|real\s+device|physical\s+device)|connect.{0,10}(to.{0,10})?(watch|device|phone|ipad|simulator)|device.{0,10}(not.{0,10}(connect|found|recogn|appear)|won.t.{0,10}(connect|appear|show))|cannot find symbol|cannot find.{0,15}in scope|use of unresolved identifier|undefined (symbol|reference)|works? (fine )?(in|on) (the )?simulator.{0,40}(fail|crash|broken|wrong|black|empty|hang).{0,15}(on|in).{0,10}(real |physical )?device|(crash|fail|broken|wrong|black|empty|hang)\w*\s+only\s+on\s+.{0,15}device|only\s+(crash|fail|broken|hang)\w*\s+(on|in)\s+.{0,15}device|(real|physical)\s*device[- ]only|device[- ]only.{0,15}(crash|fail|broken)|after .{0,30}(updating|upgrading|installing) xcode', prompt_lower):
|
||||
matches.append("axiom-build")
|
||||
|
||||
# UI
|
||||
if re.search(r'swiftui|@state\b|@binding\b|@observable\b|@environment\b|navigationstack|navigationsplitview|layout.{0,10}(break|bug|wrong|issue)|preview.{0,5}(crash|fail|not |won.t|broken)|view.{0,10}(not|won.t|doesn.t).{0,10}(updat|render|show|appear)|tabview|scroll.{0,20}(jank|lag|slow|stutter)|presentationdetents?|\bdetents?\b|presentation(compactadaptation|sizing|backgroundinteraction)|popover.{0,20}(sheet|iphone|compact|anchor)|sheet.{0,20}(detent|resiz|medium|half|landscape.{0,15}full)|onhover|hovereffect|oncontinuoushover|pointerstyle|keyframeanimator|keyframetimeline|alignmentguide|layoutpriority|toolbarminimiz\w*|(scroll\s*position|selection|focus|draft|state)\w*\s*(is\s*|gets\s*|was\s*)?(lost|reset\w*|jumps?|cleared)\w*.{0,30}(rotat|resiz|window|split|size\s*class)|(rotat|resiz)\w*.{0,30}(los\w+|reset\w*|clears?|clearing)\s.{0,20}(scroll|selection|focus|state|draft)', prompt_lower):
|
||||
matches.append("axiom-swiftui")
|
||||
|
||||
# UI — iPhone Duo (hub: axiom-swiftui skills/iphone-duo.md). API tokens are Apple-only
|
||||
# identifiers, so they stay UNGATED. Several are announced for the iOS 27.1 SDK and absent
|
||||
# from released SDKs; they still route, because the hub is where Claude learns not to write
|
||||
# them as compiling code. No bare "duo" token — Duo two-factor prompts are not iPhone Duo.
|
||||
if "axiom-swiftui" not in matches and re.search(r'iphone[\s-]*duo|onhingechange|uihingeinteraction|uiviewreservedregion|uiarrangementviewcontroller|axisbehavior|toolbarverticaledge|verticalbaredge|toolbarverticalbehavior|preferredverticalbarbehavior|verticalbarcompression\w*|toolbarverticalcompression\w*|cameracaptureaccessory|sceneaccessory|externalnoninteractiveaccessory|defaulttabbarplacement', prompt_lower):
|
||||
matches.append("axiom-swiftui")
|
||||
|
||||
# Nav-bar subtitles (iOS 26; skills/toolbars.md). navigationSubtitle and a `placement:`
|
||||
# subtitle are Apple-only spellings. Bare "largeSubtitle" is a plausible prop name anywhere,
|
||||
# so it takes the non_ios gate; it also routes to UIKit, where UINavigationItem has one, unless
|
||||
# written as navigationItem.largeSubtitle. Accepted loss: the gate only knows the frameworks
|
||||
# in non_ios_keyword, so "my JSX header's largeSubtitle prop" still routes. A plain-language
|
||||
# nav-bar subtitle question names no framework, so it routes to both suites.
|
||||
nav_subtitle_phrase = ios_signal and re.search(r'subtitle.{0,30}(nav(igation)?\s*(bar|title|item)|large\s*title)|(nav(igation)?\s*(bar|title|item)|large\s*title).{0,30}subtitle', prompt_lower)
|
||||
if "axiom-swiftui" not in matches and (re.search(r'navigationsubtitle|placement:\s*\.(largesubtitle|largetitle|subtitle)\b', prompt_lower) or nav_subtitle_phrase or (not non_ios and re.search(r'(?<!navigationitem\.)\blargesubtitle\b', prompt_lower))):
|
||||
matches.append("axiom-swiftui")
|
||||
|
||||
# "ArrangementView" (Ableton Live's timeline view) and bare "reservedRegions" (a hardware
|
||||
# memory-map term) are not Apple-only, unlike the fully-qualified UI*/ tokens above. Require
|
||||
# Apple-platform context — the Duo hub's own vocabulary (duo/fold/hinge) counts, since these
|
||||
# two tokens name the Duo feature being asked about.
|
||||
if "axiom-swiftui" not in matches and (ios_signal or re.search(r'\bduo\b|\bfold(ed|ing|s)?\b|\bunfold\w*|\bhinge\b', prompt_lower)) and re.search(r'arrangementview|reservedregions?', prompt_lower):
|
||||
matches.append("axiom-swiftui")
|
||||
|
||||
# "foldable" is cross-platform vocabulary (Android/Samsung foldables), so it takes the
|
||||
# non_ios gate, like every other generic-term rule.
|
||||
if "axiom-swiftui" not in matches and not non_ios and re.search(r'foldable|folding\s*(iphone|ipad|device|display|screen)', prompt_lower):
|
||||
matches.append("axiom-swiftui")
|
||||
|
||||
# UI — preview construction (separate from preview-crash routing above)
|
||||
# Routes to axiom-swiftui for building good previews, perf, @Previewable, PreviewModifier, variant matrix
|
||||
if "axiom-swiftui" not in matches and re.search(r'@previewable\b|previewable\s*\(\s*\)|previewmodifier|makesharedcontext|preview.{0,15}(slow|takes? \w+ seconds?|takes? forever|too slow|hang|never finish)|slow.{0,10}preview|#preview\b|preview\s+(variant|matrix|trait|modifier|canvas)|variant\s*mode|preview\s*pin|xcode_running_for_previews|development assets|sizethatfitslayout', prompt_lower):
|
||||
@@ -219,7 +248,7 @@ if not non_ios and "axiom-networking" not in matches and re.search(r'api.{0,5}(c
|
||||
matches.append("axiom-networking")
|
||||
|
||||
# Testing
|
||||
if re.search(r'xctest|xcuitest|swift\s*testing|@test\b|@suite\b|#expect\b|ui\s*test.{0,10}(fail|flak|slow|crash|record)|test.{0,10}(without simulator|faster|speed)|pseudoloc|nsdoublelocalizedstrings|appletextdirection|applelanguages|test\w*.{0,25}(resiz|window\s*sizes|rtl|right.to.left)|(resiz|rtl).{0,20}test', prompt_lower):
|
||||
if re.search(r'xctest|xcuitest|swift\s*testing|@test\b|@suite\b|#expect\b|ui\s*test.{0,10}(fail|flak|slow|crash|record)|test.{0,10}(without simulator|faster|speed)|xcuivoiceoverservice|voiceoverservice|voice\s*over.{0,30}(ui\s*test|xctest|assert|spoken|utterance)|(ui\s*test|xctest).{0,30}voice\s*over|pseudoloc|nsdoublelocalizedstrings|appletextdirection|applelanguages|test\w*.{0,25}(resiz|window\s*sizes|rtl|right.to.left)|(resiz|rtl).{0,20}test', prompt_lower):
|
||||
matches.append("axiom-testing")
|
||||
|
||||
# Integration
|
||||
@@ -234,12 +263,15 @@ if not non_ios and "axiom-integration" not in matches and re.search(r'background
|
||||
matches.append("axiom-integration")
|
||||
|
||||
# Media
|
||||
if re.search(r'avcapture|phpicker|photospicker|photo.{0,5}(library|picker|capture)|core\s*haptics|haptic|now\s*playing|shazamkit|audio\s*recogni|avfoundation|carplay.{0,12}(audio|now|map\s*panel|charging|mini\s*player|overlay)|cpmappanel|allowsminiplayer|cpchargingstation|musickit|camera.{0,5}(capture|preview|session|app|launch)|front\s*camera|center\s*stage|deferred\s*start|pro\s*video\s*storage|prores\b|smart\s*framing|dockkit|dockaccessory|dock\s*accessory|motorized.{0,12}(stand|dock)|playbackcoordinator|avdelegatingplaybackcoordinator|avplayerplaybackcoordinator|avplaybackcoordinationmedium|avcoordinatedplaybacksuspension|coordinatewithsession|coordinated\s*playback|playback\s*coordinat\w*|shareplay.{0,40}(playback|play|audio|video|media|sync|track|queue|position|timeline)|(playback|audio|video|media|sync|timeline).{0,40}shareplay|media\s*intelligence|facegroupanalyzer|videoanalyzer|highlightanalysisrequest|keyframeanalysisrequest|musicunderstanding|instrumentactivity', prompt_lower):
|
||||
if re.search(r'avcapture|phpicker|photospicker|photo.{0,5}(library|picker|capture)|core\s*haptics|haptic|now\s*playing|shazamkit|audio\s*recogni|avfoundation|carplay.{0,12}(audio|now|map\s*panel|charging|mini\s*player|overlay)|cpmappanel|allowsminiplayer|cpchargingstation|musickit|camera.{0,5}(capture|preview|session|app|launch)|center\s*stage|deferred\s*start|pro\s*video\s*storage|prores\b|smart\s*framing|dockkit|dockaccessory|dock\s*accessory|motorized.{0,12}(stand|dock)|playbackcoordinator|avdelegatingplaybackcoordinator|avplayerplaybackcoordinator|avplaybackcoordinationmedium|avcoordinatedplaybacksuspension|coordinatewithsession|coordinated\s*playback|playback\s*coordinat\w*|shareplay.{0,40}(playback|play|audio|video|media|sync|track|queue|position|timeline)|(playback|audio|video|media|sync|timeline).{0,40}shareplay|media\s*intelligence|facegroupanalyzer|videoanalyzer|highlightanalysisrequest|keyframeanalysisrequest|musicunderstanding|instrumentactivity|avcapturedevicedirectioncoordinator|avcapturedevicedescriptor|setexposuremodecustom|enabledexposuresignals|avcapturedeviceexposuresignal|continuousautofocustracking|cinematicvideometadata|lowlightvideonoisereduction|avcaptureancillarydata|recommendedlensaperturestops', prompt_lower):
|
||||
matches.append("axiom-media")
|
||||
|
||||
# Media — generic terms gated (MediaIntelligence, MusicUnderstanding: "face grouping",
|
||||
# "highlight reel", "key frames", "photos by person", "tempo", "bpm", "beatsPerMinute",
|
||||
# "chorus/verse" also name OpenCV/ffmpeg/video-editing, heart-rate, and agile concepts).
|
||||
# "chorus/verse" also name OpenCV/ffmpeg/video-editing, heart-rate, and agile concepts;
|
||||
# "inner/outer ultra wide" and "front camera" are also Android/Kotlin foldable-camera English;
|
||||
# the bare "front camera" must live HERE, not in the ungated rule above — an ungated substring
|
||||
# wins before any gated superstring can help.
|
||||
# \b on `face` keeps interface/surface out; the lookbehind on `beat detect` keeps the
|
||||
# spaced "heart beat detection" out (\b already blocks the closed "heartbeat"); the
|
||||
# highlight, key-frame, tempo, bpm, and beatsPerMinute forms require detection verbs
|
||||
@@ -256,11 +288,25 @@ if not non_ios and "axiom-media" not in matches and re.search(
|
||||
r'|\btempo\b.{0,40}(song|music|audio|bpm|beats?\b)'
|
||||
r'|(song|music|audio|tempo).{0,30}\bbpm\b|\bbpm\b.{0,30}(song|music|audio|tempo)'
|
||||
r'|(song|music|audio|tempo|rhythm)\w*.{0,40}beatsperminute|beatsperminute.{0,40}(song|music|audio|tempo|rhythm)'
|
||||
r'|(song|music|track)\w*.{0,30}\b(chorus|verse)\b', prompt_lower):
|
||||
r'|(song|music|track)\w*.{0,30}\b(chorus|verse)\b'
|
||||
r'|(inner|outer)\s*ultra\s*wide|front\s*camera'
|
||||
# 27-cycle capture controls. "lens aperture" and "aperture priority" are
|
||||
# photography vocabulary either way, so routing them to the media suite is right;
|
||||
# "exposure signals" and "autofocus tracking" also read as finance/marketing and
|
||||
# generic CV English, so those two require camera/capture proximity.
|
||||
r'|lens\s*aperture|aperture\s*priority'
|
||||
r'|(exposure\s*signals?|auto\s?focus\s*tracking).{0,40}(camera|capture|avcapture|device|scene|video)'
|
||||
r'|(camera|capture|avcapture|video).{0,40}(exposure\s*signals?|auto\s?focus\s*tracking)'
|
||||
r'|low.?light\s*video\s*noise\s*reduction|cinematic\s*video\s*metadata', prompt_lower):
|
||||
matches.append("axiom-media")
|
||||
|
||||
# Accessibility
|
||||
if re.search(r'voiceover|accessibility.{0,10}(label|hint|trait|value|issue|audit|fix)|dynamic type|color contrast|wcag|a11y|accessib.{0,10}(element|identif|action)|speak\s*screen|spoken\s*content|accessibility\s*reader|larger\s*text\b|accessibility\s*nutrition|\bdirect\s*touch|activation\s*point|button\s*shapes?\b|large\s*content\s*viewer|uilargecontentviewer|full\s*keyboard\s*access|switch\s*control\b(?!\s*(flow|statement|logic))|accessibilitysortpriority|(subtitle|caption)\s*(styl|font|color|appearance|preview)|generated\s*subtitle|(subtitle|caption)s?.{0,15}(video|player|avplayer)', prompt_lower):
|
||||
if re.search(r'voiceover|accessibility.{0,10}(label|hint|trait|value|issue|audit|fix)|dynamic type|color contrast|wcag|a11y|accessib.{0,10}(element|identif|action)|speak\s*screen|spoken\s*content|accessibility\s*reader|larger\s*text\b|accessibility\s*nutrition|\bdirect\s*touch|activation\s*point|button\s*shapes?\b|large\s*content\s*viewer|uilargecontentviewer|full\s*keyboard\s*access|switch\s*control\b(?!\s*(flow|statement|logic))|accessibilitysortpriority|generated\s*subtitle|(subtitle|caption)s?.{0,15}(video|player|avplayer)', prompt_lower):
|
||||
matches.append("axiom-accessibility")
|
||||
|
||||
# Caption styling. A navigation-bar subtitle is UI text, not a media caption, so styling
|
||||
# questions about one belong to the nav-subtitle rules above, not here.
|
||||
if "axiom-accessibility" not in matches and not re.search(r'nav(igation)?\s*(bar|item|title)|large\s*title|navigationsubtitle|largesubtitle', prompt_lower) and re.search(r'\b(subtitle|caption)\s*(styl|font|color|appearance|preview)', prompt_lower):
|
||||
matches.append("axiom-accessibility")
|
||||
|
||||
# AI
|
||||
@@ -338,7 +384,12 @@ if not non_ios and "axiom-shipping" not in matches and re.search(r'retention\s*m
|
||||
# macOS
|
||||
# Note: bare "macos"/"mac os" is intentionally NOT matched — it fires on host-OS
|
||||
# version mentions ("on macOS 26.3"). Require intent-qualifying terms instead.
|
||||
if re.search(r'mac\s*app(?:lication)?s?\b|macos.{0,15}(app|build|sandbox|develop|distribut|notariz|menubar|window|toolbar|sign)|appkit|screencapturekit|scstream\b|scshareablecontent|sccontentfilter|sccontentsharingpicker|scscreenshotmanager|screcordingoutput|nstoolbar|nsviewrepresentable|nshostingcontroller|nshostingview|nshostingmenu|nshostingscene|nsgesturerecognizerrepresentable|nsviewcontrollerrepresentable|nscontrol\b|nsstatusitem|status\s*items?\b.{0,40}(window|menu|keyboard|expand)|menu\s*bar.{0,15}status\s*item|nswindowrestoration|encoderestorablestate|nsrefreshcontroller|nstextselectionmanager|nsglasseffect|cornerconfiguration|concentric.{0,10}corner|corner.{0,12}concentric|windowgroup|menubarextra|utilitywindow|commandmenu|commandgroup|focusedscenevalue|app\s*sandbox|sandbox.{0,10}(violat|entitlement|bookmark)|security.{0,5}scoped|notariz|notarytool|developer\s*id|hardened\s*runtime|sparkle.{0,5}(update|framework|auto)|\.dmg\b|distribut.{0,10}outside|menu\s*bar.{0,5}(extra|command|item)|\bcatalyst\b|maccatalyst|designed\s*for\s*ip(?:ad|hone)|ios\s*apps?\s*on\s*(?:apple\s*silicon|mac)|isiosapponmac|tablecolumn|swiftui\s*table|multi.?column\s*table|\.inspector\b|inspector\s*(column|panel|pane)|navigationsubtitle|navigationdocument|uidocumentproperties|proxy\s*icon|uiprintinteraction\w*|nsprintoperation|backingscalefactor', prompt_lower):
|
||||
if re.search(r'mac\s*app(?:lication)?s?\b|macos.{0,15}(app|build|sandbox|develop|distribut|notariz|menubar|window|toolbar|sign)|appkit|(cross.?team|(another|other|different)\s*team\S{0,3}).{0,20}app\s*container|app\s*container.{0,25}(cross.?team|(another|other|different)\s*team)|screencapturekit|scstream\b|scshareablecontent|sccontentfilter|sccontentsharingpicker|scscreenshotmanager|screcordingoutput|nstoolbar|nsviewrepresentable|nshostingcontroller|nshostingview|nshostingmenu|nshostingscene|nsgesturerecognizerrepresentable|nsviewcontrollerrepresentable|nscontrol\b|nsstatusitem|status\s*items?\b.{0,40}(window|menu|keyboard|expand)|menu\s*bar.{0,15}status\s*item|nswindowrestoration|encoderestorablestate|nsrefreshcontroller|nstextselectionmanager|nsglasseffect|cornerconfiguration|concentric.{0,10}corner|corner.{0,12}concentric|windowgroup|menubarextra|utilitywindow|commandmenu|commandgroup|focusedscenevalue|app\s*sandbox|sandbox.{0,10}(violat|entitlement|bookmark)|security.{0,5}scoped|notariz|notarytool|developer\s*id|hardened\s*runtime|sparkle.{0,5}(update|framework|auto)|\.dmg\b|distribut.{0,10}outside|menu\s*bar.{0,5}(extra|command|item)|\bcatalyst\b|maccatalyst|designed\s*for\s*ip(?:ad|hone)|ios\s*apps?\s*on\s*(?:apple\s*silicon|mac)|isiosapponmac|tablecolumn|swiftui\s*table|multi.?column\s*table|\.inspector\b|inspector\s*(column|panel|pane)|navigationdocument|uidocumentproperties|proxy\s*icon|uiprintinteraction\w*|nsprintoperation|backingscalefactor', prompt_lower):
|
||||
matches.append("axiom-macos")
|
||||
|
||||
# navigationSubtitle is iPhone/iPad API too (axiom-swiftui skills/toolbars.md), so it spends a
|
||||
# macOS slot only with Mac or window-title context (skills/windows.md).
|
||||
if "axiom-macos" not in matches and re.search(r'navigationsubtitle', prompt_lower) and re.search(r'\bmac|catalyst|window|title\s*bar', prompt_lower):
|
||||
matches.append("axiom-macos")
|
||||
|
||||
# watchOS
|
||||
@@ -358,8 +409,31 @@ if re.search(r'apple\s*pay|pkpayment|pkpaymentauthorization|passkit|\bpkpass\b|w
|
||||
if re.search(r'human interface|hig\b|liquid glass|glass\s*[-]?\s*effect\b|glasseffectcontainer|glasseffectlayer|sf symbol|symbol.{0,5}(effect|variablevalue|render)|typography.{0,10}(ios|swift|app)|design.{0,5}(system|pattern|token)|app.{0,5}(entry|onboard)|launch\s*(screen|image|storyboard)\b|app\s*launch\s*(experience|animation|sequence)\b|authentication.{0,5}(flow|screen|ui)|concentric.{0,10}(corner|rectangle)|corner.{0,12}concentric', prompt_lower):
|
||||
matches.append("axiom-design")
|
||||
|
||||
# The Liquid Glass opt-out key (skills/liquid-glass.md). Its name, in any spelling, is Apple-only
|
||||
# and stays ungated. "Design compatibility key/mode/flag/setting" is also AutoCAD, Figma, Ant
|
||||
# Design, and SQL Server vocabulary, so it needs an iOS signal or a plist mention; "key" is not
|
||||
# context ("registry key", "config key"). Opting out of / disabling / turning off "the new
|
||||
# design" needs an OS 26/27 version or a plist mention, because without one it is usually the
|
||||
# app's own redesign. Accepted loss: phrasings with no Apple context or no version.
|
||||
os_27_cycle = re.search(r'\b(i|ipad|mac|watch|tv|vision)?os\s*2[67]\b', prompt_lower)
|
||||
if "axiom-design" not in matches and (re.search(r'designrequirescompat', prompt_lower) or ((ios_signal or re.search(r'\bplist', prompt_lower)) and re.search(r'design[\s-]*compat[ai]bility\b.{0,15}\b(key|mode|flag|setting)s?\b', prompt_lower)) or ((os_27_cycle or re.search(r'\bplist', prompt_lower)) and re.search(r'(opt\w*[\s-]*out|disabl\w*|turn\w*\s*off)\b.{0,25}\bnew\s*((i|ipad|mac)?os\s*2[67][.\d]*\s*)?(design|look|appearance)\b', prompt_lower))):
|
||||
matches.append("axiom-design")
|
||||
|
||||
# UIKit
|
||||
if re.search(r'uikit|uiview\b|uiviewcontroller|auto\s*layout|nslayoutconstraint|uiviewrepresentable|uihostingcontroller|combine\b.{0,10}(publisher|subscriber|sink|assign)|textkit|nstextlayoutmanager|uilabel|uitableview|uicollectionview|pencilkit|pkcanvasview|pktoolpicker|pkdrawing|apple\s*pencil|paperkit|papermarkup|uicornerconfiguration|cornerconfiguration|encoderestorablestate|iphone\s*mirroring|indirectinputevents|(uiscene)?sizerestrictions|uipointerinteraction|uihovergesture|uikeycommand|discoverabilitytitle|uiapplicationscenemanifest|scene\s*manifest|readablecontentguide|uilayoutguide|layoutmarginsguide|nscollectionlayout|compositional\s*layout|self.?sizing\s*cell|systemlayoutsizefitting|scenediddisconnect|staterestorationactivity|activatescenesession|uiscenesessionactivationrequest|activeappearance|uiscribble\w*|\bscribble\b.{0,30}(handwrit|pencil|text|disable)|per.?(window|scene)\s*undo|undo\s*manager.{0,35}(window|scene)|(window|scene).{0,35}undo\s*manager|undo\w*\s.{0,20}(another|other|wrong)\s*window|resiz\w*.{0,20}(window|scene|screen|apps?\b|layout|support|readiness|ready|mode|model)|(window|scene|screen|apps?\b|layout|free.?form).{0,20}resiz\w*|foldable|folding\s*(iphone|ipad|device|display|screen)', prompt_lower):
|
||||
if re.search(r'uikit|uiview\b|uiviewcontroller|auto\s*layout|nslayoutconstraint|uiviewrepresentable|uihostingcontroller|combine\b.{0,10}(publisher|subscriber|sink|assign)|textkit|nstextlayoutmanager|uilabel|uitableview|uicollectionview|pencilkit|pkcanvasview|pktoolpicker|pkdrawing|apple\s*pencil|paperkit|papermarkup|uicornerconfiguration|cornerconfiguration|encoderestorablestate|iphone\s*mirroring|indirectinputevents|(uiscene)?sizerestrictions|uipointerinteraction|uihovergesture|uikeycommand|discoverabilitytitle|uiapplicationscenemanifest|scene\s*manifest|readablecontentguide|uilayoutguide|layoutmarginsguide|nscollectionlayout|compositional\s*layout|self.?sizing\s*cell|systemlayoutsizefitting|scenediddisconnect|staterestorationactivity|activatescenesession|uiscenesessionactivationrequest|activeappearance|uiscribble\w*|\bscribble\b.{0,30}(handwrit|pencil|text|disable)|per.?(window|scene)\s*undo|undo\s*manager.{0,35}(window|scene)|(window|scene).{0,35}undo\s*manager|undo\w*\s.{0,20}(another|other|wrong)\s*window|resiz\w*.{0,20}(window|scene|screen|apps?\b|layout|support|readiness|ready|mode|model)|(window|scene|screen|apps?\b|layout|free.?form).{0,20}resiz\w*|uitoolbar|uinavigationbar|uitabbar\w*|uibarbuttonitem', prompt_lower):
|
||||
matches.append("axiom-uikit")
|
||||
|
||||
# "foldable" is cross-platform vocabulary — gated. It shipped ungated in the rule above
|
||||
# and routed Android/Kotlin foldable prompts to UIKit (test_foldable_vocabulary_is_gated).
|
||||
if "axiom-uikit" not in matches and not non_ios and re.search(r'foldable|folding\s*(iphone|ipad|device|display|screen)', prompt_lower):
|
||||
matches.append("axiom-uikit")
|
||||
|
||||
# Nav-bar subtitles (iOS 26; skills/uikit-modernization.md). The large/attributed variants and
|
||||
# appearance keys are Apple-only. Bare "largeSubtitle" is a plausible prop name, so it takes the
|
||||
# non_ios gate and skips the `.largeSubtitle` placement spelling. Bare "subtitleView" is not
|
||||
# Apple-only (Media3/ExoPlayer ships one, and Android has navigation bars too), so it needs an
|
||||
# iOS signal or UIKit's own navigation-item/large-title vocabulary.
|
||||
if "axiom-uikit" not in matches and (re.search(r'uinavigationitem|largesubtitleview|attributedsubtitle|subtitletextattributes|navigationitem\.\w*subtitle', prompt_lower) or nav_subtitle_phrase or (not non_ios and re.search(r'(?<!\.)\blargesubtitle\b', prompt_lower)) or ((ios_signal or re.search(r'navigation\s*item|large\s*title', prompt_lower)) and re.search(r'\bsubtitleview\b', prompt_lower))):
|
||||
matches.append("axiom-uikit")
|
||||
|
||||
# Swift language
|
||||
@@ -400,8 +474,10 @@ if not matches:
|
||||
print("{}")
|
||||
sys.exit(0)
|
||||
|
||||
# Dedupe before capping — some rules (AI, ML) append the same skill unguarded, and a
|
||||
# duplicate would waste one of the 3 slots below. dict.fromkeys preserves first-seen order.
|
||||
# Limit to top 3 matches (more is noise)
|
||||
matches = matches[:3]
|
||||
matches = list(dict.fromkeys(matches))[:3]
|
||||
|
||||
if len(matches) == 1:
|
||||
skill = matches[0]
|
||||
|
||||
@@ -47,6 +47,7 @@ Delegate to the appropriate Cursor subagent when this router calls for a special
|
||||
|
||||
- Full watchOS development context → See axiom-watchos
|
||||
- Live accessibility validation on the simulator (set toggles, assert announcements) → `simulator-tester` agent + `xcui` — see axiom-tools (skills/xcui-ref.md)
|
||||
- Asserting VoiceOver speech in an XCUITest (`XCUIDevice.shared.voiceOverService`, `OS27`) → axiom-testing (skills/ui-testing.md Pattern 7)
|
||||
|
||||
## Decision Tree
|
||||
|
||||
|
||||
@@ -550,6 +550,4 @@ These are not adapter-specific — they affect any `@Generable` usage. Apply the
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-05-16
|
||||
**Toolkit Version**: 26.0.0
|
||||
**Skill Type**: Diagnostic
|
||||
|
||||
@@ -687,7 +687,5 @@ For server-hosted delivery (`BADownloaderExtension`), see `axiom-integration (sk
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-11
|
||||
**Toolkit Version**: 26.0.0
|
||||
**Platforms**: iOS / iPadOS / macOS / visionOS **26.0–26.x only** (runtime deprecated 26.4, obsoleted 27.0; never watchOS/tvOS); macOS 14+ Apple silicon ≥32 GB or Linux GPU (training)
|
||||
**Skill Type**: Reference
|
||||
|
||||
@@ -435,6 +435,4 @@ Without per-locale eval, none of these are measurable until App Store reviews su
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-11
|
||||
**Platforms**: iOS / iPadOS / macOS / visionOS **26.0–26.x only** (runtime deprecated 26.4, obsoleted 27.0; never watchOS/tvOS); macOS 14+ Apple silicon ≥32 GB or Linux GPU (training)
|
||||
**Skill Type**: Discipline
|
||||
|
||||
@@ -1230,9 +1230,3 @@ Post-mortem items:
|
||||
- WWDC 2025-286: Meet the Foundation Models framework
|
||||
- WWDC 2025-301: Deep dive into the Foundation Models framework
|
||||
- Instruments Foundation Models Template
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-07-12
|
||||
**Version**: 1.0.0
|
||||
**Skill Type**: Diagnostic
|
||||
|
||||
@@ -183,7 +183,6 @@ precondition(k != -1, "alignment was never computed — check the label")
|
||||
#expect(k > 0.6)
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Tool Metrics Read `-1` and the Suite Is Green
|
||||
@@ -391,6 +390,4 @@ Five errors specific to this framework:
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-07-12
|
||||
**Platforms**: iOS / iPadOS / macOS / watchOS / visionOS 27+ (not tvOS)
|
||||
**Skill Type**: Diagnostic
|
||||
**Platforms**: OS27, not tvOS
|
||||
|
||||
@@ -586,6 +586,4 @@ expectation.allowsAdditionalCalls = false
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-07-12
|
||||
**Platforms**: iOS / iPadOS / macOS / watchOS / visionOS 27+ (not tvOS)
|
||||
**Skill Type**: Reference
|
||||
**Platforms**: OS27, not tvOS
|
||||
|
||||
@@ -610,6 +610,4 @@ That is one long afternoon and it is the difference between shipping with a numb
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-07-12
|
||||
**Platforms**: iOS / iPadOS / macOS / watchOS / visionOS 27+ (not tvOS)
|
||||
**Skill Type**: Discipline
|
||||
**Platforms**: OS27, not tvOS
|
||||
|
||||
@@ -1525,6 +1525,4 @@ Use `@Generable` with `respond(to:generating:)` instead of prompting for JSON an
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-09
|
||||
**Skill Type**: Reference
|
||||
**Content**: WWDC 2025 + 2026 code examples; OS27 surface verified against the Xcode 27 SDK
|
||||
|
||||
@@ -1336,5 +1336,4 @@ Before shipping Foundation Models features:
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-09
|
||||
**Target**: iOS 26+, macOS 26+, iPadOS 26+, visionOS 26+; OS27 surface verified against the Xcode 27 SDK
|
||||
|
||||
+12
-4
@@ -173,6 +173,18 @@ rm Package.resolved
|
||||
swift package resolve
|
||||
```
|
||||
|
||||
### Issue 6: `-ld_classic` Silently Ignored (Xcode 27)
|
||||
|
||||
**Symptom**: a target that passes `-ld_classic` in `OTHER_LDFLAGS` still links, but behaves as though the flag were absent. The build log carries `ld: warning: -ld_classic is no longer supported and will be ignored`.
|
||||
|
||||
**Cause**: Xcode 27 removed the classic linker (ld64). The toolchain ships a single `ld`, and the flag is accepted and discarded. This is a **warning, not an error** — so a project pinned to classic-linker behavior keeps building green while quietly getting the new linker, and only the downstream symptom (duplicate symbols, `-force_load` / `-all_load` ordering, dead-strip differences) shows up.
|
||||
|
||||
**Fix**: drop `-ld_classic` from `OTHER_LDFLAGS`, xcconfigs, and CI scripts, then re-test whatever the flag was masking — that original problem is still there and now needs a real fix. The older `-ld64` spelling is worse: it parses as `-l d64` and fails the link with `library 'd64' not found`.
|
||||
|
||||
Verified on Xcode 27.0 (27A266a): `clang -Wl,-ld_classic` exits 0 with that warning, and `ld -v` reports `PROJECT:ld-27037.1`.
|
||||
|
||||
**Same release, adjacent trap**: Apple's 27 release notes state the Swift dependency scanner now requires **every Clang module reachable from a single dependency-scan action to have a unique module name**; duplicates were previously tolerated and can now error the scan. If scanning started failing right after the upgrade, look for two modules vending the same name rather than for a code change — typically vendored third-party sources that ship a `module.modulemap` redeclaring an SDK module.
|
||||
|
||||
## Dependency Resolution Strategies
|
||||
|
||||
### Strategy 1: Lock to Specific Versions
|
||||
@@ -500,7 +512,3 @@ xcodebuild -showBuildSettings # Show all build settings
|
||||
**GitHub**: Carthage/Carthage
|
||||
|
||||
**Skills**: axiom-build (skills/xcode-debugging.md)
|
||||
|
||||
---
|
||||
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -1344,11 +1344,11 @@ func fastBridge() async -> Data {
|
||||
|
||||
**Use checked continuations during development, switch to unsafe only after thorough testing and when profiling shows the check is a bottleneck.**
|
||||
|
||||
### Single-Resume `Continuation` (OS27 — present but limited in beta)
|
||||
### Single-Resume `Continuation` (OS27 — limited in 27.0)
|
||||
|
||||
Swift 6.4 adds `withContinuation`, vending a `~Copyable` `Continuation` whose `resume` methods are `consuming` — so a **double-resume is a build error** (`'c' consumed more than once`) rather than a runtime crash. It is the statically-checked successor to `withCheckedContinuation`.
|
||||
|
||||
**Not yet a drop-in replacement in the current Xcode 27 beta.** Because the continuation is `~Copyable`, resuming it from an `@escaping` callback does not compile — `noncopyable 'c' cannot be consumed when captured by an escaping closure or borrowed by a non-Escapable type` — which is exactly the delegate / completion-handler bridging that `withCheckedContinuation` is for. Only synchronous resume inside the `withContinuation` body works today; never-resuming is **not** diagnosed.
|
||||
**Not a drop-in replacement in 27.0.** Because the continuation is `~Copyable`, resuming it from an `@escaping` callback does not compile — `noncopyable 'c' cannot be consumed when captured by an escaping closure or borrowed by a non-Escapable type` — which is exactly the delegate / completion-handler bridging that `withCheckedContinuation` is for. Only synchronous resume inside the `withContinuation` body works today; never-resuming is **not** diagnosed.
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@@ -1360,7 +1360,7 @@ func value() async -> Int {
|
||||
}
|
||||
```
|
||||
|
||||
Keep `withCheckedContinuation` for real callback bridging until a later beta lifts the escaping-closure limitation. Re-probe each beta.
|
||||
Keep `withCheckedContinuation` for real callback bridging: Xcode 27.0 (swiftlang-6.4.0.34.1) ships with the escaping-closure limitation (verified at SIL). On a newer toolchain, compile the commented escaping line before relying on it.
|
||||
|
||||
### Continuation Gotcha Table
|
||||
|
||||
|
||||
@@ -1392,8 +1392,3 @@ You do **not** have to refactor everything into a "perfect" concurrency model be
|
||||
**Docs**: /swift/adoptingswift6, /swift/sendable, /foundation/progressmanager, /foundation/subprogress
|
||||
|
||||
**Skills**: axiom-build (skills/lldb.md) (debug actor/task state in the debugger)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-01
|
||||
**Status**: Enhanced with WWDC 2025-268 progressive journey, @concurrent attribute, isolated conformances, and approachable concurrency patterns
|
||||
|
||||
@@ -649,8 +649,3 @@ func diagnoseCloudSyncIssue() async {
|
||||
- `skills/cloudkit-ref.md` — CloudKit implementation details
|
||||
- `skills/icloud-drive-ref.md` — iCloud Drive implementation details
|
||||
- `skills/storage.md` — Choose sync approach
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Diagnostic
|
||||
|
||||
@@ -673,7 +673,5 @@ func userDidAcceptCloudKitShareWith(_ cloudKitShareMetadata: CKShare.Metadata) {
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 10.0 (basic), 17.0 (CKSyncEngine, SwiftData integration)
|
||||
**WWDC Sessions**: 2023-10188 (CKSyncEngine), 2024-10122 (CloudKit Console)
|
||||
|
||||
@@ -948,7 +948,5 @@ This protects you and shows you exhausted safe options first.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-30
|
||||
**Status**: TDD-tested with pressure scenarios
|
||||
**Framework**: Core Data (Foundation framework)
|
||||
**Complements**: SwiftData skill (understanding relationship to Core Data)
|
||||
|
||||
@@ -462,6 +462,4 @@ See axiom-swift (skills/tvos.md) for full tvOS storage constraints.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-28
|
||||
**Frameworks**: SQLite, GRDB, SwiftData
|
||||
**Status**: Production-ready patterns for safe schema evolution
|
||||
|
||||
-1
@@ -789,4 +789,3 @@ The write case *is* a mistake, and it's the first entry above: N inserts outside
|
||||
|
||||
**Targets:** Axiom floor — iOS 18+/macOS 15+
|
||||
**Framework:** GRDB.swift 7.9+ (current 7.11.1), Swift 6.1+, Xcode 16.3+
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -482,6 +482,4 @@ class ICloudMonitor {
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 5.0 (basic), 8.0 (iCloud Drive), 11.0 (modern APIs)
|
||||
|
||||
@@ -787,7 +787,5 @@ Settings → [Your Name] → iCloud → Check CloudKit status
|
||||
|
||||
---
|
||||
|
||||
**Created**: 2025-11-30
|
||||
**Status**: Production-ready migration guide
|
||||
**Urgency**: Realm Device Sync sunset September 30, 2025
|
||||
**Estimated Migration Time**: 2-8 weeks depending on app complexity
|
||||
|
||||
@@ -279,7 +279,3 @@ let project = Project.find(task.projectID)
|
||||
- `skills/sqlitedata.md` — Full SQLiteData API reference
|
||||
- `skills/swiftdata.md` — SwiftData patterns if staying with Apple's framework
|
||||
- `skills/grdb.md` — Raw GRDB for complex queries
|
||||
|
||||
---
|
||||
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -1026,4 +1026,3 @@ let shared = try Customer.select(\.email).intersect(Supplier.select(\.email)).fe
|
||||
|
||||
**Targets:** iOS 17+, Swift 6
|
||||
**Framework:** SQLiteData 1.12+ (StructuredQueries 0.39+, GRDB 7.11+)
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -1051,4 +1051,3 @@ SQLiteData with CloudKit SyncEngine is the **recommended tvOS data solution**. t
|
||||
|
||||
**Targets:** iOS 17+, Swift 6
|
||||
**Framework:** SQLiteData 1.12+ (StructuredQueries 0.39+, GRDB 7.11+)
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -400,8 +400,3 @@ func diagnoseStorageIssue(fileURL: URL) {
|
||||
- `skills/storage.md` — Correct storage location decisions
|
||||
- axiom-security (skills/file-protection-ref.md) — Understanding protection levels
|
||||
- `skills/storage-management-ref.md` — Purge behavior and capacity APIs
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-05-21
|
||||
**Skill Type**: Diagnostic
|
||||
|
||||
@@ -599,6 +599,4 @@ print("Protection: \(values.fileProtection ?? .none)")
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 5.0 (basic), 11.0 (modern capacity APIs)
|
||||
|
||||
@@ -546,8 +546,6 @@ When changing storage approach:
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Discipline
|
||||
**Related WWDC Sessions**:
|
||||
- WWDC 2023-10187: Meet SwiftData
|
||||
- WWDC 2023-10188: Sync to iCloud with CKSyncEngine
|
||||
|
||||
@@ -270,4 +270,3 @@ Consequence worth knowing: SQLiteData depends on swift-sharing 2.3.0+, so a proj
|
||||
|
||||
**Targets:** iOS 26+/18, macOS 26+/15
|
||||
**Framework:** Sharing 2.10+ (manifests back to Swift 5.9; traits need 6.1+)
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -586,7 +586,5 @@ If you've spent >30 minutes and the migration issue persists:
|
||||
|
||||
---
|
||||
|
||||
**Created** 2025-12-09
|
||||
**Status** Production-ready diagnostic patterns
|
||||
**Framework** SwiftData (Apple)
|
||||
**Swift** 5.9+
|
||||
|
||||
@@ -1009,7 +1009,6 @@ let container = try ModelContainer(
|
||||
|
||||
---
|
||||
|
||||
**Created** 2025-12-09
|
||||
**Targets** iOS 17+ (focus on iOS 26+ features)
|
||||
**Framework** SwiftData (Apple)
|
||||
**Swift** 5.9+
|
||||
|
||||
+9
-6
@@ -18,10 +18,12 @@ Delegate to the appropriate Cursor subagent when this router calls for a special
|
||||
|----------------|-----------|
|
||||
| Design decisions, HIG compliance, colors, backgrounds | See `skills/hig.md` |
|
||||
| Spacing, padding, margins, insets — what value to use | See `skills/hig.md` |
|
||||
| Designing for iPhone Duo — poses, controls on the side, keeping UI out of the fold | See axiom-swiftui (skills/iphone-duo.md) |
|
||||
| Section index / A–Z index strip / alphabet scrubber — should this list have one | See `skills/hig.md` |
|
||||
| Semantic colors, custom color patterns, material styles | See `skills/hig-ref.md` |
|
||||
| Liquid Glass effects, adoption, migration from blur effects | See `skills/liquid-glass.md` |
|
||||
| App-wide Liquid Glass adoption, backward compatibility | See `skills/liquid-glass-ref.md` |
|
||||
| App-wide Liquid Glass adoption, platform differences | See `skills/liquid-glass-ref.md` |
|
||||
| Backward compatibility: the `UIDesignRequiresCompatibility` opt-out, and why the 27 SDK ignores it on OS 27 | See `skills/liquid-glass.md` (Backward Compatibility) |
|
||||
| SF Symbols rendering modes, effects, animations | See `skills/sf-symbols.md` |
|
||||
| SF Symbols API signatures, UIKit equivalents, availability | See `skills/sf-symbols-ref.md` |
|
||||
| Verify a symbol name exists / check one symbol's availability or supported modes (`sfsymbols` CLI) | See `skills/sf-symbols-ref.md` (Part 11) |
|
||||
@@ -40,8 +42,8 @@ digraph design {
|
||||
what -> "skills/hig.md" [label="design decision,\nHIG compliance,\ncolor/background choice"];
|
||||
what -> "skills/hig.md" [label="spacing/padding/margin value"];
|
||||
what -> "skills/hig-ref.md" [label="semantic color API,\ncustom color code,\nmaterial style details"];
|
||||
what -> "skills/liquid-glass.md" [label="Liquid Glass effects,\nmigrate from blur,\nRegular vs Clear"];
|
||||
what -> "skills/liquid-glass-ref.md" [label="app-wide Liquid Glass plan,\nplatform differences,\nbackward compat"];
|
||||
what -> "skills/liquid-glass.md" [label="Liquid Glass effects,\nmigrate from blur,\nRegular vs Clear,\nopt-out key"];
|
||||
what -> "skills/liquid-glass-ref.md" [label="app-wide Liquid Glass plan,\nplatform differences"];
|
||||
what -> "skills/sf-symbols.md" [label="rendering mode choice,\nsymbol effects/animations,\ncustom symbols"];
|
||||
what -> "skills/sf-symbols-ref.md" [label="SF Symbols API syntax,\nUIKit equivalents,\navailability matrix"];
|
||||
what -> "skills/typography-ref.md" [label="font selection,\nDynamic Type,\ntext styles, tracking"];
|
||||
@@ -52,7 +54,8 @@ digraph design {
|
||||
1. Design decision / HIG compliance / choosing colors or backgrounds? → `skills/hig.md`
|
||||
1a. Need semantic color API, custom color code, or material style details? → `skills/hig-ref.md`
|
||||
2. Liquid Glass effects / migrating from blur / Regular vs Clear variant? → `skills/liquid-glass.md`
|
||||
2a. Planning app-wide Liquid Glass adoption / platform differences / backward compatibility? → `skills/liquid-glass-ref.md`
|
||||
2a. Planning app-wide Liquid Glass adoption / platform differences? → `skills/liquid-glass-ref.md`
|
||||
2b. Opting out of Liquid Glass / `UIDesignRequiresCompatibility` / backward compatibility? → `skills/liquid-glass.md` (Backward Compatibility)
|
||||
3. SF Symbols rendering mode / symbol effects / custom symbols? → `skills/sf-symbols.md`
|
||||
3a. Need SF Symbols API syntax / UIKit equivalents / availability check? → `skills/sf-symbols-ref.md`
|
||||
4. Font selection / Dynamic Type / text styles / tracking / leading? → `skills/typography-ref.md`
|
||||
@@ -60,7 +63,7 @@ digraph design {
|
||||
6. SwiftUI view implementation? → `/skill axiom-swiftui`
|
||||
7. TextKit / rich text editing / Writing Tools? → `/skill axiom-uikit`
|
||||
8. Accessibility compliance (VoiceOver, contrast, touch targets)? → `/skill axiom-accessibility`
|
||||
9. Audit UI for Liquid Glass adoption? → liquid-glass-auditor (Agent — surfaces migration opportunities AND adoption-completeness gaps: variant discipline, nesting hygiene, availability gating, primary-action tinting, accessibility re-check; scores ADOPTED / PARTIAL / NOT ADOPTED)
|
||||
9. Audit UI for Liquid Glass adoption? → liquid-glass-auditor (Agent — surfaces migration opportunities AND adoption-completeness gaps: variant discipline, nesting hygiene, unstyled pre-26 fallbacks, semantic toolbar placement, the `UIDesignRequiresCompatibility` opt-out, accessibility re-check; scores ADOPTED / PARTIAL / NOT ADOPTED)
|
||||
10. CarPlay app design, categories, driver-distraction rules? → `/skill axiom-media` (carplay-hig.md)
|
||||
|
||||
#### Platform-specific HIG
|
||||
@@ -109,11 +112,11 @@ digraph design {
|
||||
- Migration strategy from pre-iOS 26 materials
|
||||
- Tinting, legibility, and adaptive behavior troubleshooting
|
||||
- Expert review criteria for Liquid Glass implementations
|
||||
- The `UIDesignRequiresCompatibility` opt-out and its measured SDK × OS behavior
|
||||
|
||||
**Liquid Glass Adoption** (`skills/liquid-glass-ref.md`):
|
||||
- App-wide adoption planning (icons, controls, navigation, menus)
|
||||
- Platform-specific behavior (iOS, iPadOS, macOS, tvOS, watchOS)
|
||||
- Backward compatibility strategy for supporting pre-Liquid Glass
|
||||
- Accessibility compliance with Liquid Glass interfaces
|
||||
|
||||
**SF Symbols** (`skills/sf-symbols.md`):
|
||||
|
||||
@@ -778,6 +778,8 @@ struct ItemRow: View {
|
||||
}
|
||||
```
|
||||
|
||||
On iPhone and iPad, show new-window buttons only when `@Environment(\.supportsMultipleWindows)` is true. iPhone Duo can open new windows only from its inner display, and the talks don't say how that display reports this value — see axiom-swiftui (skills/iphone-duo.md, Gate new-window affordances).
|
||||
|
||||
### Dismissing Windows Programmatically
|
||||
|
||||
```swift
|
||||
|
||||
+1
-6
@@ -1222,13 +1222,8 @@ HStack {
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 356, 2019-808
|
||||
**WWDC**: 2025-356, 2019-808
|
||||
|
||||
**Docs**: /design/human-interface-guidelines, /design/human-interface-guidelines/color, /design/human-interface-guidelines/dark-mode, /design/human-interface-guidelines/materials, /design/human-interface-guidelines/typography, /design/human-interface-guidelines/layout, /design/human-interface-guidelines/accessibility, /design/human-interface-guidelines/icons, /design/human-interface-guidelines/apple-pay, /design/human-interface-guidelines/wallet, /design/human-interface-guidelines/tap-to-pay-on-iphone
|
||||
|
||||
**Skills**: axiom-design (skills/hig.md), axiom-design (skills/liquid-glass.md), axiom-design (skills/liquid-glass-ref.md), axiom-swiftui, axiom-accessibility, axiom-swift (skills/tvos.md), axiom-payments
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: Based on Apple HIG (2024-2025), WWDC25-356, WWDC19-808
|
||||
**Skill Type**: Reference (Comprehensive guide with code examples)
|
||||
|
||||
+4
-9
@@ -13,12 +13,12 @@ Use when:
|
||||
|
||||
#### Related Skills
|
||||
- Use `axiom-design (skills/hig-ref.md)` for comprehensive details and code examples
|
||||
- Use `axiom-design (skills/liquid-glass.md)` for iOS 26 material design implementation and version-conditional design (supporting both pre-Liquid Glass and Liquid Glass in the same app)
|
||||
- Use `axiom-design (skills/liquid-glass-ref.md)` for iOS 26 app-wide adoption guide with backward compatibility strategy
|
||||
- Use `axiom-design (skills/liquid-glass.md)` for iOS 26 material design implementation, version-conditional design (supporting both pre-Liquid Glass and Liquid Glass in the same app), and the `UIDesignRequiresCompatibility` opt-out
|
||||
- Use `axiom-design (skills/liquid-glass-ref.md)` for iOS 26 app-wide adoption guide
|
||||
- Use `axiom-accessibility` for accessibility troubleshooting
|
||||
|
||||
#### Version-Conditional Design
|
||||
When supporting both iOS 25 (pre-Liquid Glass) and iOS 26+, see `axiom-design (skills/liquid-glass.md)` for the adoption strategy — it covers when to use `#available(iOS 26, *)`, how to degrade gracefully, and which system components adopt Liquid Glass automatically vs which need explicit opt-in.
|
||||
When supporting both iOS 18 (pre-Liquid Glass) and iOS 26+, see `axiom-design (skills/liquid-glass.md)` for the adoption strategy — it covers when to use `#available(iOS 26, *)`, how to degrade gracefully, and which system components adopt Liquid Glass automatically vs which need explicit opt-in.
|
||||
|
||||
---
|
||||
|
||||
@@ -496,13 +496,8 @@ Apple Pay, Wallet, and Tap to Pay each have their own HIG with rules App Review
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 356, 2019-808
|
||||
**WWDC**: 2025-356, 2019-808
|
||||
|
||||
**Docs**: /design/human-interface-guidelines, /design/human-interface-guidelines/layout, /design/human-interface-guidelines/lists-and-tables, /design/human-interface-guidelines/color, /design/human-interface-guidelines/dark-mode, /design/human-interface-guidelines/typography, /design/human-interface-guidelines/apple-pay, /design/human-interface-guidelines/wallet, /design/human-interface-guidelines/tap-to-pay-on-iphone
|
||||
|
||||
**Skills**: axiom-design (skills/hig-ref.md), axiom-design (skills/liquid-glass.md), axiom-design (skills/liquid-glass-ref.md), axiom-swiftui (skills/26-ref.md), axiom-accessibility, axiom-payments
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: Based on Apple HIG (2024-2025), WWDC25-356, WWDC19-808
|
||||
**Skill Type**: Discipline (Quick decisions, checklists, pressure scenarios)
|
||||
|
||||
+8
-10
@@ -445,7 +445,7 @@ List(items) { item in
|
||||
|
||||
### Semantic Search Tabs
|
||||
|
||||
For Tab API patterns including `.tabRole(.search)`, see `axiom-swiftui` navigation reference, Section 5 (Tab Navigation Integration).
|
||||
For Tab API patterns including `Tab(role: .search)`, see `axiom-swiftui` navigation reference, Section 5 (Tab Navigation Integration).
|
||||
|
||||
---
|
||||
|
||||
@@ -550,7 +550,7 @@ Profile scrolling, animations, memory, and CPU with Instruments (Time Profiler,
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
Add `UIDesignRequiresCompatibility = true` to Info.plist to ship with iOS 26 SDK while maintaining iOS 18 appearance (Liquid Glass disabled, previous blur/material styles used). Migration strategy: ship with key enabled, audit changes in separate build, update incrementally, remove key when ready.
|
||||
Add `UIDesignRequiresCompatibility = true` to Info.plist to keep the pre-Liquid-Glass appearance temporarily (previous blur/material styles). The system ignores the key when the app is built with the 27 SDK and runs on OS 27; it still applies on 26.x (measured on iOS), and for apps still built with the 26 SDK. Migration strategy: audit in a build without the key, update incrementally, remove the key, and finish before or with the move to the 27 SDK. Details and the measured SDK × OS matrix: `skills/liquid-glass.md` (Backward Compatibility).
|
||||
|
||||
---
|
||||
|
||||
@@ -578,7 +578,7 @@ Add `UIDesignRequiresCompatibility = true` to Info.plist to ship with iOS 26 SDK
|
||||
### Navigation
|
||||
- [ ] `.tabViewStyle(.sidebarAdaptable)` - Tab bar adapts to sidebar
|
||||
- [ ] `.tabBarMinimizeBehavior(_:)` - Minimize on scroll
|
||||
- [ ] `.tabRole(.search)` - Semantic search tabs
|
||||
- [ ] `Tab(role: .search)` - Semantic search tabs
|
||||
- [ ] `NavigationSplitView` for sidebar + inspector layouts
|
||||
|
||||
### Toolbars and Menus
|
||||
@@ -597,7 +597,7 @@ Add `UIDesignRequiresCompatibility = true` to Info.plist to ship with iOS 26 SDK
|
||||
- [ ] Test with accessibility settings
|
||||
|
||||
### Backward Compatibility
|
||||
- [ ] `UIDesignRequiresCompatibility` in Info.plist (if needed)
|
||||
- [ ] `UIDesignRequiresCompatibility` in Info.plist only if needed, and adoption finished before or with the move to the 27 SDK (which ignores the key on OS 27)
|
||||
|
||||
---
|
||||
|
||||
@@ -648,7 +648,7 @@ Use this checklist when auditing app for Liquid Glass adoption. 30 highest-impac
|
||||
- [ ] watchOS: Standard toolbar APIs and button styles adopted
|
||||
- [ ] tvOS: Standard focus APIs for Liquid Glass on focus
|
||||
- [ ] `GlassEffectContainer` used for multiple nearby glass effects
|
||||
- [ ] `UIDesignRequiresCompatibility` key considered if needed
|
||||
- [ ] `UIDesignRequiresCompatibility` key considered if needed (ignored on OS 27 in 27-SDK builds)
|
||||
|
||||
---
|
||||
|
||||
@@ -656,7 +656,7 @@ Use this checklist when auditing app for Liquid Glass adoption. 30 highest-impac
|
||||
|
||||
**WWDC**: 2025-219, 2025-323 (Build a SwiftUI app with the new design)
|
||||
|
||||
**Docs**: /TechnologyOverviews/liquid-glass, /TechnologyOverviews/adopting-liquid-glass, /design/Human-Interface-Guidelines/materials
|
||||
**Docs**: /TechnologyOverviews/liquid-glass, /TechnologyOverviews/adopting-liquid-glass, /design/Human-Interface-Guidelines/materials, /bundleresources/information-property-list/uidesignrequirescompatibility
|
||||
|
||||
**Sample Code**: /SwiftUI/Landmarks-Building-an-app-with-Liquid-Glass
|
||||
|
||||
@@ -664,7 +664,5 @@ Use this checklist when auditing app for Liquid Glass adoption. 30 highest-impac
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-01
|
||||
**Minimum Platform**: iOS/iPadOS 26, macOS Tahoe 26, tvOS 26, watchOS 26 (`glassEffect` is unavailable on visionOS; visionOS uses `glassBackgroundEffect`)
|
||||
**Xcode Version**: Xcode 26+
|
||||
**Skill Type**: Reference (comprehensive adoption guide)
|
||||
**Platforms**: OS26, not visionOS (visionOS uses `glassBackgroundEffect`)
|
||||
**Xcode**: 26+
|
||||
|
||||
+21
-13
@@ -538,7 +538,7 @@ Documented platform behaviors with no reliable fix from user code as of iOS 26.0
|
||||
|
||||
**Workarounds** (tradeoffs, not fixes):
|
||||
- Match the launch-screen color to your app background — papers over the cold-start flash only; does nothing for the pop-back or tab-switch cases.
|
||||
- Set `UIDesignRequiresCompatibility=true` — forces the pre-Liquid-Glass appearance but disables Liquid Glass app-wide (heavy hammer; see the UIDesignRequiresCompatibility section under Backward Compatibility below).
|
||||
- Set `UIDesignRequiresCompatibility=true` — forces the pre-Liquid-Glass appearance but disables Liquid Glass app-wide (heavy hammer, and ignored on iOS 27 once the app builds with the 27 SDK; see the UIDesignRequiresCompatibility section under Backward Compatibility below).
|
||||
|
||||
**Do not reach for `UITabBar.appearance()`**: `UITabBar.appearance().standardAppearance` was the iOS 15–25 escape hatch for forcing SwiftUI `TabView` bar appearance. On iOS 26's floating pill this bridge is unreliable — the new render path does not consistently honor the proxy — so it is a dead end here, not a workaround.
|
||||
|
||||
@@ -588,24 +588,33 @@ See `axiom-design (skills/liquid-glass-ref.md)` for complete UIBlurEffect migrat
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
### UIDesignRequiresCompatibility Key (iOS 26)
|
||||
### UIDesignRequiresCompatibility Key
|
||||
|
||||
To ship with latest SDKs while maintaining previous appearance:
|
||||
A temporary opt-out from the new design (iOS, iPadOS, macOS, tvOS 26):
|
||||
|
||||
```xml
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
**Effect**: App built with iOS 26 SDK, appearance matches iOS 18 and earlier, Liquid Glass effects disabled, previous blur/material styles used.
|
||||
**Effect**: the system runs the app in a UI compatibility mode, so it looks as it did when built against earlier SDKs; system bars and controls use their previous styles.
|
||||
|
||||
**When to use**: Need time to audit interface changes, gradual adoption strategy, or maintain exact appearance temporarily.
|
||||
**Building with the 27 SDK turns the key off on OS 27.** Apple's documentation: "The system ignores this key when you build for iOS 27 or later, iPadOS 27 or later, Mac Catalyst 27 or later, macOS 27 or later, or tvOS 27 or later." What decides it is the SDK the app links together with the OS it runs on, not the deployment target. Measured on iPhone simulators with the key set; the 26 SDK row is an emulated 26-SDK build (the binary's Mach-O SDK version rewritten with `vtool`, minimum OS unchanged):
|
||||
|
||||
| Built with | Runs on iOS 26.5 | Runs on iOS 27.0 |
|
||||
|---|---|---|
|
||||
| 26 SDK | compatibility mode | compatibility mode |
|
||||
| 27 SDK | compatibility mode | new design, key ignored |
|
||||
|
||||
Consequences:
|
||||
- Updating to Xcode 27 ends the opt-out for iOS 27 users. Finish adopting the new design before or with that SDK update, not after.
|
||||
- A 27-SDK app that keeps the key shows compatibility mode on iOS 26 and the new design on iOS 27, so it ships two appearances; test both. Code that needs the new design (navigation subtitles don't render in compatibility mode) runs only under `#available(iOS 27, *)` while the key remains, with a fallback on 26.x.
|
||||
- Staying on the 26 SDK only postpones this. App Store Connect periodically raises the minimum SDK required for uploads, and the first update built with the 27 SDK loses compatibility mode on OS 27.
|
||||
|
||||
**Migration strategy**:
|
||||
1. Ship with `UIDesignRequiresCompatibility` enabled
|
||||
2. Audit interface changes in separate build
|
||||
3. Update interface incrementally
|
||||
4. Remove key when ready for Liquid Glass
|
||||
1. While on the 26 SDK, ship with the key only if you need time; audit in a build without it
|
||||
2. Update the interface incrementally, then remove the key
|
||||
3. Complete adoption before or with the move to the 27 SDK, where the key stops applying on OS 27
|
||||
|
||||
---
|
||||
|
||||
@@ -619,12 +628,11 @@ For complete API reference including `glassEffect()`, `GlassEffectContainer`, `g
|
||||
|
||||
**WWDC**: 2025-219, 2025-256, 2025-323 (Build a SwiftUI app with the new design)
|
||||
|
||||
**Docs**: /technologyoverviews/adopting-liquid-glass, /swiftui/landmarks-building-an-app-with-liquid-glass, /swiftui/applying-liquid-glass-to-custom-views
|
||||
**Docs**: /technologyoverviews/adopting-liquid-glass, /swiftui/landmarks-building-an-app-with-liquid-glass, /swiftui/applying-liquid-glass-to-custom-views, /bundleresources/information-property-list/uidesignrequirescompatibility
|
||||
|
||||
**Skills**: axiom-design (skills/liquid-glass-ref.md)
|
||||
|
||||
---
|
||||
|
||||
**Platforms:** iOS 26+, iPadOS 26+, macOS Tahoe 26+, tvOS 26+, watchOS 26+ (`glassEffect` is unavailable on visionOS; only `backgroundExtensionEffect` is offered there)
|
||||
**Xcode:** 26+
|
||||
**History:** See git log for changes
|
||||
**Platforms**: OS26, not visionOS (`glassEffect` is unavailable there; only `backgroundExtensionEffect` is offered)
|
||||
**Xcode**: 26+
|
||||
|
||||
@@ -1042,5 +1042,4 @@ Symbol names are easy to invent and the compiler never checks them: `Image(syste
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** Based on WWDC 2023/10257-10258, WWDC 2024/10188, WWDC 2025/337
|
||||
**Version** iOS 13+ (display), iOS 15+ (rendering modes), iOS 17+ (effects), iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw, Gradients)
|
||||
|
||||
@@ -594,5 +594,4 @@ Image(systemName: isFavorite ? "star.fill" : "star")
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** Based on WWDC 2023/10257-10258, WWDC 2024/10188, WWDC 2025/337
|
||||
**Version** iOS 17+ (effects), iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw On/Off, Variable Draw, Gradients)
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ For 3D content in non-game SwiftUI apps, AR experiences, and spatial computing,
|
||||
- RealityRenderer for custom Metal rendering of RealityKit content
|
||||
- Complete material property reference
|
||||
- RealityView gesture integration
|
||||
- RealityKit 27 additions: navigation mesh, LOD, soft shadows, Gaussian splats (visionOS), reverb meshes (Part 10), cloth simulation (`ClothBodyComponent`), ComputeGraph framework
|
||||
- RealityKit 27 additions: navigation mesh, LOD, soft shadows, Gaussian splats, reverb meshes (Part 10), cloth simulation (`ClothBodyComponent`), ComputeGraph framework
|
||||
|
||||
**usdkit** (USD authoring, 27 releases):
|
||||
- USDStage open/traverse/edit, references and composition
|
||||
|
||||
@@ -591,6 +591,4 @@ When something doesn't work:
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-29
|
||||
**Platforms**: iOS 12+, macOS 10.14+, tvOS 12+
|
||||
**Status**: Comprehensive Metal porting diagnostics
|
||||
|
||||
@@ -662,12 +662,12 @@ For moving/deforming geometry, store previous-frame world positions (or skin twi
|
||||
|
||||
| `MTLTensorDataType` | Format |
|
||||
|---------------------|--------|
|
||||
| `.float8E4M3` / `.float8E5M2` | 8-bit float (4 or 5 exponent bits) |
|
||||
| `.float4E2M1` | 4-bit float |
|
||||
| `.metalFloat8e4m3` / `.metalFloat8e5m2` | 8-bit float (4 or 5 exponent bits) |
|
||||
| `.metalFloat4e2m1` | 4-bit float |
|
||||
| `.int2` / `.uint2` | 2-bit integer |
|
||||
| `.float8UE8M0` | Scale-factor format for block-wise (MX) quantization |
|
||||
| `.metalFloat8ue8m0` | Scale-factor format for block-wise (MX) quantization |
|
||||
|
||||
Scales attach as an auxiliary plane on the same tensor — `MTLTensorAuxiliaryPlaneDescriptor` (with `blockFactors`, e.g. one scale per 32×1 block) registered in an `MTLTensorAuxiliaryPlaneDescriptorMap` for `MTLTensorPlaneTypeScales`, assigned to `MTLTensorDescriptor.auxiliaryPlanes`. Note: Apple's session slide uses `MTLTensorDataTypeMetalFloat8E4M3` — the shipping header has no `Metal` infix (`MTLTensorDataTypeFloat8E4M3`). The new types carry extra alignment requirements; check the Metal docs.
|
||||
Scales attach as an auxiliary plane on the same tensor — `MTLTensorAuxiliaryPlaneDescriptor` (with `blockFactors`, e.g. one scale per 32×1 block) registered in an `MTLTensorAuxiliaryPlaneDescriptorMap` for `MTLTensorPlaneTypeScales`, assigned to `MTLTensorDescriptor.auxiliaryPlanes`. Note the spelling: the new 27 float formats carry a `Metal` infix in ObjC (`MTLTensorDataTypeMetalFloat8E4M3`), and Swift lowercases the suffix — `.metalFloat8e4m3`, `.metalFloat8e5m2`, `.metalFloat4e2m1`, `.metalFloat8ue8m0` (compile-verified on iOS and macOS 27). Beta-era spellings such as `.float8E4M3` fail with `type 'MTLTensorDataType' has no case 'float8E4M3'`. `.int4` / `.uint4` are 26.4, not 27. The new types carry extra alignment requirements; check the Metal docs.
|
||||
|
||||
MSL side, declare the plane and tensor types, then TensorOps dequantizes automatically:
|
||||
|
||||
@@ -695,6 +695,13 @@ using mxfp8_tensor = tensor<device metal_fp8_e4m3_format, dextents<int, 2>,
|
||||
| MetalFX content regions | `contentWidth/Height` (frame interpolator), `colorContentOffsetX/Y` (temporal scaler), plus depth/motion/reactive-mask/output/distortion offsets — all `API_UNAVAILABLE(visionos)` |
|
||||
| MetalFX reactive mask rename | `reactiveMaskTextureUsage` replaces deprecated `reactiveTextureUsage` |
|
||||
| MetalFX frame interpolation | `isDistortionTextureEnabled` + distortion texture/region, `requiresPrevColorTexture`, `worldToViewMatrix`/`viewToClipMatrix`; temporal scaler gains output-resolution and jittered motion-vector options |
|
||||
| Persistent-kernel compute | `optimizeForPersistentKernel` on `MTLComputePipelineDescriptor` and `MTL4ComputePipelineDescriptor`, plus `MTLComputePipelineState.recommendedPersistentThreadgroupsPerGrid(forThreadsPerThreadgroup:)` for sizing the launch. The 27.0 headers ship these with **no doc comments** — names and types are the whole of what Apple documents |
|
||||
| `MTLForwardProgressUsage` | `.automatic` / `.weak` / `.simdGroupParallel`; settable on both compute descriptors, read-only on `MTLComputePipelineState`. Undocumented beyond the case names |
|
||||
| `MTLContentionRelief` | `.automatic` / `.none`; settable on both compute descriptors. Undocumented beyond the case names |
|
||||
| Three-channel pixel formats | `.rgb8Unorm/Snorm/Uint/Sint`, `.rgb16Unorm/Snorm/Uint/Sint/Float`, `.rgb32Uint/Sint/Float`. Texture-buffer only — the descriptor's `textureType` must be `.textureBuffer` and its `usage` must not include `.shaderWrite` |
|
||||
| `minLOD` | Settable on `MTLTextureViewDescriptor`, read-only on `MTLTexture`; default `0.0`. Clamps the LOD range for views built from the descriptor, applied after sampler LOD clamping and view level-range offsets. Read needs `floor(minLOD) ≤ mip level`, gather needs `floor(minLOD) ≤ levelRange.location`, sample needs `minLOD ≤ levelRange.location + levelRange.length` — otherwise the operation returns its out-of-bounds value |
|
||||
|
||||
The last five rows need the Xcode 27.0 release SDK; the WWDC betas lacked them. Their headers name only `macos` and `ios`, but all five compile against the tvOS and visionOS 27 SDKs too — the omitted platforms inherit rather than exclude.
|
||||
|
||||
For CoreML-level model conversion, quantization, and deployment (including the 27-cycle Core AI tooling for PyTorch models with custom Metal kernels — `TorchMetalKernel`, `coreai-torch`, the `.aimodel` runtime), see axiom-ai (skills/core-ai.md) — this part covers only the Metal-side surface.
|
||||
|
||||
@@ -708,6 +715,4 @@ For CoreML-level model conversion, quantization, and deployment (including the 2
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-10
|
||||
**Platforms**: iOS 12+, macOS 10.14+, tvOS 12+
|
||||
**Status**: Complete shader conversion and API mapping reference
|
||||
|
||||
@@ -485,6 +485,4 @@ After completing the port:
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-29
|
||||
**Platforms**: iOS 12+, macOS 10.14+, tvOS 12+
|
||||
**Status**: Production-ready Metal migration patterns
|
||||
|
||||
+4
-4
@@ -701,7 +701,7 @@ func setUpCloth(simulationRoot: Entity, cloth: Entity) {
|
||||
|
||||
**Building the cloth mesh:** create geometry with `ClothMeshResource` factories — `.patch(size:)`, `.box(size:)`, `.sphere(radius:)`, `.capsule(height:radius:)`, `.cylinder(height:radius:withCaps:)`, or `init(from: MeshResource)` — then `ClothBodyComponent(mesh:meshDraping:)`, draping from a `ClothPoseResource(positions:)`. `ClothSimulationComponent` also exposes `solver` (`.gaussSeidel(iterationCount:)` / `.jacobi(iterationCount:)`), `speedLimit`, `timeStep`, and a by-name `materials` collection where `ClothBodyMaterial` / `ClothColliderMaterial` register (referenced by each component's `materialNames`). Force/query/grab volumes take a `ClothVolumeShape` (distinct from the collider's `ClothColliderShape`).
|
||||
|
||||
**Experimental API:** the entire cloth surface is annotated `@available(*, deprecated, message: "This API is experimental and may change or be removed in a future release.")` in the 27 beta. Adopting it emits deprecation warnings today and can source-break in a later beta — gate it behind your own flag and re-verify each beta.
|
||||
**Cloth ships unannotated in 27.0.** The betas marked the whole surface `@available(*, deprecated, message: "This API is experimental…")`; 27.0 carries no such annotation and cloth compiles warning-free, so remove any warning suppression added for the betas and treat the API as shipping.
|
||||
|
||||
### ComputeGraph framework (`OS27`, not watchOS)
|
||||
|
||||
@@ -762,7 +762,7 @@ Node names follow the MaterialX convention (`ND_atan2_float`), and each definiti
|
||||
| The `replace*` closures validate on return and **throw** | Joint indices vs `jointTransformCount`, adjacencies vs `indexCount / 3`, triangle indices vs `vertexCount`, and `replaceAdjacencyEndIndices` vs `renormalizing.adjacenciesCount` |
|
||||
| Buffer shapes are exact | Blend-shape offsets = `targetCount × vertexCount`; weights = `targetCount` floats |
|
||||
|
||||
Unlike cloth, this is **not** experimental — no deprecation annotations, and it ships on tvOS too. The cloth source-break caveat does not transfer.
|
||||
It ships on tvOS too, unlike cloth.
|
||||
|
||||
### Runtime Skybox and IBL Generation (`OS27`, not watchOS)
|
||||
|
||||
@@ -882,9 +882,9 @@ LevelOfDetailComponent.addByScreenArea(to: entity, levels: [
|
||||
])
|
||||
```
|
||||
|
||||
### Gaussian Splats (visionOS27)
|
||||
### Gaussian Splats `OS27`, not watchOS/tvOS
|
||||
|
||||
Renders captured volumetric scenes as 3D Gaussians. No file format is assumed — you supply per-splat buffers (position, scale, rotation, opacity, spherical harmonics plus degree; degree 0 = view-independent color). In the first 27 beta the API is present only in the visionOS SDK. Each buffer parameter is a `GaussianSplatResource.BufferDescriptor` (`LowLevelBuffer` + `MTLAttributeFormat` + stride + offset); the degree is a `SphericalHarmonicDegree` enum value.
|
||||
Renders captured volumetric scenes as 3D Gaussians. No file format is assumed — you supply per-splat buffers (position, scale, rotation, opacity, spherical harmonics plus degree; degree 0 = view-independent color). Each buffer parameter is a `GaussianSplatResource.BufferDescriptor` (`LowLevelBuffer` + `MTLAttributeFormat` + stride + offset); the degree is a `SphericalHarmonicDegree` enum value.
|
||||
|
||||
```swift
|
||||
let buffers = try GaussianSplatResource.BufferResource(
|
||||
|
||||
@@ -131,6 +131,8 @@ Every sample carries a type, a time window, a value, optional metadata, and prov
|
||||
| `.discreteTemporallyWeighted` | Heart rate | Time-weighted average (older readings matter less) |
|
||||
| `.discreteEquivalentContinuousLevel` | Audio exposure | Continuous-level average per acoustics convention |
|
||||
|
||||
**New quantity type `OS27`**: `HKQuantityTypeIdentifier.heartRateVariabilityRMSSD` (milliseconds, discrete arithmetic) ships on every platform at 27. HealthKit previously exposed only `.heartRateVariabilitySDNN`, so code and advice written before 27 that says SDNN is the only HRV metric is now out of date.
|
||||
|
||||
## Canonical Setup Pattern
|
||||
|
||||
```swift
|
||||
|
||||
@@ -112,7 +112,7 @@ struct MoodView: View {
|
||||
|
||||
## Medications API
|
||||
|
||||
**Platform:** iOS 26+, iPadOS 26+, macOS 26+, visionOS 26+, watchOS 26+
|
||||
**Platforms**: OS26, not tvOS
|
||||
|
||||
> The Health app has had medication tracking since iOS 15, but the **public Medications API is iOS 26 and later only**. Prior-OS apps cannot read or write medication data.
|
||||
|
||||
@@ -268,7 +268,7 @@ Write purpose strings that honestly describe why the app needs mental-health or
|
||||
| Averaging raw valence across a mix of negative and positive days | Shift to `[0, 2]` by `valence + 1.0` before averaging, then rescale to `[0, 100]`. |
|
||||
| Trying to request medication access via `requestAuthorization` | Medications use per-object authorization managed inside the Health app. The normal sheet does nothing for medication types. |
|
||||
| Expecting a framework API linking symptoms to medications | There isn't one. Apple's sample uses an RxNorm → symptom-list dictionary client-side. |
|
||||
| Using the Medications API on iOS 25 or earlier | API is iOS 26+. Check with `@available(iOS 26.0, *)`. |
|
||||
| Using the Medications API on iOS 18 or earlier | API is iOS 26+. Check with `@available(iOS 26.0, *)`. |
|
||||
| Assuming the Health app "one mood per day" rule reflects the framework | Daily mood samples can be saved multiple times per day via the API. The Health app UI shows one, but your data model can differ. |
|
||||
| Requesting every `HKStateOfMind.Label` and `.Association` up front | Request the minimum set for your feature. Broad requests feel invasive for mental-health data. |
|
||||
| Displaying raw valence numbers to users | Users understand emotional language, not `-0.2 to 0.8`. Map to the 7-bucket `ValenceClassification` or emoji. |
|
||||
|
||||
@@ -97,7 +97,7 @@ try await AssetPackManager.shared.ensureLocalAvailability(
|
||||
)
|
||||
```
|
||||
|
||||
The `Set`-based overload throws `AssetPackManager.LocalAvailabilityError` when any pack fails, carrying `successes: Set<AssetPack>` and `failures: [AssetPack: any Error]` so you can retry only what failed.
|
||||
The `Set`-based overload throws when the system can't ensure one or more packs' local availability. Cast with `as? AssetPackManager.LocalAvailabilityError` (documented as possible, not guaranteed); when it matches, it carries `successes: Set<AssetPack>` and `failures: [AssetPack: any Error]`, so you can retry only what failed.
|
||||
|
||||
### Status streaming
|
||||
|
||||
@@ -240,7 +240,7 @@ Localized asset packs let the system deliver only the assets matching the user's
|
||||
|
||||
When no pack matches the user-selected language exactly, the system falls back automatically:
|
||||
|
||||
1. **Regional fallback** — another variant of the same base language (e.g. user selects English-UK, no en-GB pack exists → the en-US pack is used)
|
||||
1. **Regional fallback** — another variant of the same base language (e.g. user selects English-UK, no en-GB pack exists → the en-US pack is used). Not stated in any header; the SDK says only that `localizedAssetPacks(for:)` "may not exactly match the specified language", so treat the exact selection rule as unverified
|
||||
2. **Primary app language** — if no similar regional variant exists at all (e.g. user selects Spanish, no Spanish pack and no regional variant → the app's primary language, English, is used)
|
||||
|
||||
### Manifest declaration
|
||||
@@ -272,11 +272,37 @@ Upload localized variants of your asset packs to App Store Connect to reduce per
|
||||
| Reconcile downloads after change | `AssetPackManager.shared.reconcilePreferredLanguages() async throws` |
|
||||
| Read a localized file | `contents(at:asLocalizedFor:options:)`, `descriptor(for:asLocalizedFor:)`, `url(for:asLocalizedFor:)` |
|
||||
|
||||
### Reading localized files
|
||||
|
||||
The `asLocalizedFor:` overloads exist for one situation: you deliberately ship the **same relative path** in several differently localized packs. All asset packs share a single namespace, so with split-language functionality installed, `contents(at:searchingInAssetPackWithID:options:)` would read from an undefined pack. Passing a `Locale.Language` resolves the ambiguity without making you look up a pack ID. In every other situation, prefer the `searchingInAssetPackWithID:` forms.
|
||||
|
||||
```swift
|
||||
let language = Locale.Language(identifier: "he")
|
||||
|
||||
// Searches only packs localized for `language`
|
||||
let data = try AssetPackManager.shared.contents(
|
||||
at: "Videos/Introduction.m4v",
|
||||
asLocalizedFor: language,
|
||||
options: .mappedIfSafe
|
||||
)
|
||||
|
||||
// Only the URL form resolves whole directories (including packages),
|
||||
// merging the matching slices from every localized pack
|
||||
let url = try AssetPackManager.shared.url(
|
||||
for: "Videos",
|
||||
asLocalizedFor: language
|
||||
)
|
||||
```
|
||||
|
||||
Language matching follows Unicode CLDR implicit script and region tags: `en` matches `en-US` and `en-Latn-US`, but **not** `en-CA`. All three overloads are `nonisolated` and synchronous, like their non-localized siblings. `url(for:asLocalizedFor:)` is the least efficient of the three and returns a well-formed URL even when nothing exists at the path. It is synchronous, so call it off the main thread, and don't use it to reach the namespace root.
|
||||
|
||||
### Behavior notes
|
||||
|
||||
- `resolvedLanguage` respects a language your app sets manually; set it to `nil` to revert to the user's system-wide preference. Setting it does **not** immediately download or remove packs — call `reconcilePreferredLanguages()` to reconcile.
|
||||
- `reconcilePreferredLanguages()` downloads missing localized packs, waits for those downloads, and removes unneeded ones. Don't use it if your app offers split-language functionality — handle reconciliation manually in that case.
|
||||
- Setting `resolvedLanguage` also changes the app's **display language**. Never set it from inside your downloader extension.
|
||||
- `reconcilePreferredLanguages()` downloads missing localized packs, waits for those downloads, and removes unneeded ones. Don't use it if your app offers split-language functionality — handle reconciliation manually in that case. It won't remove localized packs you downloaded manually, and it can throw `LocalAvailabilityError` (possible, not guaranteed) when some or all packs fail to reconcile.
|
||||
- If the user recently changed their preferred language, `resolvedLanguage` can be temporarily out of sync with the set of locally available packs.
|
||||
- The extension side of a language change arrives as `BAContentRequest.languageChange` — see the BAContentRequest table below.
|
||||
|
||||
---
|
||||
|
||||
@@ -360,7 +386,7 @@ import ExtensionFoundation
|
||||
@main
|
||||
struct DownloaderExtension: BADownloaderExtension {
|
||||
// The scheduling entry point: the system asks which downloads to start
|
||||
// for an install / periodic / update content request.
|
||||
// for an install / update / periodic / language-change content request.
|
||||
func downloads(
|
||||
for request: BAContentRequest,
|
||||
manifestURL: URL,
|
||||
@@ -426,6 +452,32 @@ try manager.startForegroundDownload(download)
|
||||
try manager.scheduleDownload(download)
|
||||
```
|
||||
|
||||
### Exclusive control
|
||||
|
||||
`BADownloadManager` is shared between the app and its downloader extension, so both sides must serialize access or they schedule against each other. Both must call this API for it to mean anything — one-sided use buys nothing.
|
||||
|
||||
```swift
|
||||
// Acquire, run the body, relinquish
|
||||
let scheduled = try await BADownloadManager.shared.withExclusiveControl { @Sendable in
|
||||
try BADownloadManager.shared.scheduleDownload(download)
|
||||
return true
|
||||
}
|
||||
|
||||
// Give up if control can't be acquired by a deadline.
|
||||
// Passing Date() attempts acquisition and fails instantly if unavailable.
|
||||
try await BADownloadManager.shared.withExclusiveControl(
|
||||
before: Date().addingTimeInterval(5)
|
||||
) { @Sendable in
|
||||
try BADownloadManager.shared.scheduleDownload(download)
|
||||
}
|
||||
```
|
||||
|
||||
The body must be `@Sendable`: the overlay is `@concurrent`, so an unannotated closure written in main-actor code fails Swift 6 with "sending value of non-Sendable type".
|
||||
|
||||
These `async` overloads are Swift-overlay additions carrying `@backDeployed(before: iOS 27, macOS 27, tvOS 27, visionOS 27)`, so building with the 27 SDK makes them callable from an **iOS 16.1 / macOS 13 / tvOS 18.4 / visionOS 2.4** deployment target. New spelling, not new availability — do not gate them behind `if #available(iOS 27, *)`.
|
||||
|
||||
The completion-handler forms they replace — `withExclusiveControl { acquired, error in }` and `withExclusiveControl(beforeDate:perform:)` — are **deprecated in Swift at 27** (each carries its own message — "Use the asynchronous overload of `withExclusiveControl(_:)` instead" and "Use `withExclusiveControl(before:_:)` instead"). The deprecation is Swift-only; the Objective-C `performWithExclusiveControl:` spelling is not deprecated. Unavailable on watchOS.
|
||||
|
||||
### BAURLDownload
|
||||
|
||||
```swift
|
||||
@@ -463,16 +515,35 @@ public struct Priority { /* RawRepresentable over Int */ }
|
||||
|
||||
### BAContentRequest
|
||||
|
||||
The framework distinguishes three content-request types, delivered to your extension's `downloads(for:manifestURL:extensionInfo:)` scheduling entry point:
|
||||
The framework distinguishes four content-request types, delivered to your extension's `downloads(for:manifestURL:extensionInfo:)` scheduling entry point:
|
||||
|
||||
```swift
|
||||
public enum BAContentRequest {
|
||||
case install // First-install event
|
||||
case periodic // System-scheduled periodic refresh
|
||||
case update // App-update event
|
||||
case install // raw value 1
|
||||
case update // 2
|
||||
case periodic // 3
|
||||
case languageChange // 4
|
||||
}
|
||||
```
|
||||
|
||||
| Case | Delivered when |
|
||||
|------|----------------|
|
||||
| `install` | The app was installed |
|
||||
| `update` | The app was updated |
|
||||
| `periodic` | The system requests updated content within the app |
|
||||
| `languageChange` `OS27` | Someone changed the app's preferred language |
|
||||
|
||||
`languageChange` is the extension-side counterpart to localized asset packs: it fires when the preferred language changes, so the extension can schedule the newly needed language variants. The case is iOS 27 / macOS 27 / tvOS 27 / visionOS 27 (iPadOS follows iOS); the enum itself dates to iOS 16.1 and is unavailable on watchOS.
|
||||
|
||||
What recompiling a 26-era `switch` against the 27 SDK costs you depends on its shape:
|
||||
|
||||
| The 26-era switch | Building against the 27 SDK |
|
||||
|---|---|
|
||||
| No `@unknown default` | **Build break** — `error: switch must be exhaustive ... add missing case: '.languageChange'` |
|
||||
| Already has `@unknown default` | Compiles — `warning: switch must be exhaustive` |
|
||||
|
||||
In Swift 6 language mode you need **both** the new case and `@unknown default`: adding only the case gives `error: switch covers known cases, but 'BAContentRequest' may have additional unknown values, possibly added in future versions`. And `@unknown default` on its own silently routes real language-change requests into the default branch, which is how this change turns into a bug rather than a build error. Pattern position is safe for older deployment targets — `case .languageChange:` compiles unguarded down to iOS 16.1; only using the case as a *value* needs a version gate.
|
||||
|
||||
---
|
||||
|
||||
## Info.plist Keys
|
||||
@@ -606,6 +677,8 @@ public enum ManagedBackgroundAssetsError: CustomStringConvertible, LocalizedErro
|
||||
| `assetPackNotFound` | Pack ID not present in manifest (or not yet downloaded) | Verify `assetPackID` matches manifest and server response |
|
||||
| `fileNotFound` | File missing within an otherwise-available pack | Verify `fileSelectors` in manifest match the path you're querying |
|
||||
|
||||
`ManagedBackgroundAssetsError` has exactly these two Swift cases. Objective-C carries a third code, `BAManagedErrorCodeLocalAvailabilityFailure`, whose Swift surface is the separate `AssetPackManager.LocalAvailabilityError` struct `OS27` — which the `Set`-based `ensureLocalAvailability` and `reconcilePreferredLanguages()` may throw. Its `successes` / `failures` properties refine away the `BASuccessesErrorKey` and `BAFailuresErrorKey` `userInfo` keys, so Swift code never reads those keys directly.
|
||||
|
||||
### BAErrorCode
|
||||
|
||||
```swift
|
||||
@@ -978,7 +1051,8 @@ struct CustomDownloaderExtension: BADownloaderExtension {
|
||||
- **`AssetPackManager.LocalAvailabilityError`** (27) — `successes: Set<AssetPack>`, `failures: [AssetPack: any Error]`
|
||||
- **`AssetPack.Status`** — `OptionSet` flags (membership-test, don't switch): `downloadAvailable`, `downloading`, `downloaded`, `upToDate`, `outOfDate`, `obsolete`, `updateAvailable`; stream-only `DownloadStatusUpdate` enum cases (unlabeled payloads): `began(AssetPack)`, `paused(AssetPack)`, `downloading(AssetPack, Progress)`, `finished(AssetPack)`, `failed(AssetPack, Error)`
|
||||
- **Extensions** — `StoreDownloaderExtension` (Apple-hosted), `BADownloaderExtension` (server-hosted), `ManagedDownloaderExtension` (parent)
|
||||
- **Unmanaged types** — `BADownloadManager`, `BAURLDownload`, `BADownload`, `BADownload.State`, `BADownload.Priority`, `BAContentRequest`
|
||||
- **Unmanaged types** — `BADownloadManager`, `BAURLDownload`, `BADownload`, `BADownload.State`, `BADownload.Priority`, `BAContentRequest` (`.install`, `.update`, `.periodic`, `.languageChange` (27))
|
||||
- **Exclusive control** — `BADownloadManager.withExclusiveControl(_:)` / `withExclusiveControl(before:_:)` — `async` Swift-overlay overloads, back-deployed to iOS 16.1 / macOS 13 / tvOS 18.4 / visionOS 2.4; the completion-handler spellings are deprecated in Swift at 27
|
||||
- **Errors** — `ManagedBackgroundAssetsError.assetPackNotFound`, `.fileNotFound`; `BAErrorCode.downloadAlreadyScheduled`, `.downloadBackgroundActivityProhibited`, `.downloadWouldExceedAllowance`, `.sessionDownloadAllowanceExceeded`
|
||||
- **Info.plist** — `BAHasManagedAssetPacks`, `BAUsesAppleHosting`, `BAAppGroupID`, `BAManifestURL`, `BAEssentialMaxInstallSize`, `BAMaxInstallSize`, `BAInitialDownloadRestrictions`
|
||||
- **Tooling** — `xcrun ba-package template`, `xcrun ba-package <manifest> -o <archive>`, `xcrun ba-package download-manifest` (self-hosted), `xcrun ba-package convert` (Steam `.vdf` → manifest, Xcode 27), `xcrun ba-package evaluate` (Xcode 27), `xcrun ba-serve --host <host> <archives...>`, `xcrun ba-serve url-override <url>`, Xcode 27 auto-attached mock server (scheme Run settings)
|
||||
@@ -996,7 +1070,5 @@ struct CustomDownloaderExtension: BADownloaderExtension {
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-11
|
||||
**Platforms**: iOS 26+, iPadOS 26+, macOS 26+, tvOS 26+, visionOS 26+ (managed); iOS 16.1+ (unmanaged legacy)
|
||||
**Skill Type**: Reference
|
||||
**Platforms**: OS26 for managed assets, not watchOS; iOS 16.1+ for unmanaged legacy
|
||||
**Content**: All public APIs, Info.plist keys, manifest schema, tooling commands, Foundation Models adapter bridge
|
||||
|
||||
@@ -53,6 +53,7 @@ If you see ANY of these, you're heading toward either an oversized IPA, a brittl
|
||||
- **Bundling Foundation Models `.fmadapter` files in the app bundle**: Apple explicitly prohibits this — adapters are ~160 MB and per-OS-version; bundle bloat compounds across OS versions
|
||||
- **Assuming asset packs auto-evict**: The system does NOT remove asset packs while your app is installed; call `remove(assetPackWithID:)` when done
|
||||
- **Hyphens in adapter names**: The identifier regex `/fmadapter-\w+-\w+/` breaks on hyphens; use underscores
|
||||
- **Taking exclusive control on only one side**: `BADownloadManager.withExclusiveControl` serializes your app against its downloader extension, and the header is explicit — "Both the extension and app must use this API to ensure exclusive access." A one-sided call buys nothing; the other side still schedules underneath you
|
||||
|
||||
---
|
||||
|
||||
@@ -231,7 +232,7 @@ try await AssetPackManager.shared.ensureLocalAvailability(of: assetPack)
|
||||
let descriptor = try AssetPackManager.shared.descriptor(
|
||||
for: "Videos/Introduction.m4v"
|
||||
)
|
||||
defer { try descriptor.close() }
|
||||
defer { try? descriptor.close() }
|
||||
```
|
||||
|
||||
**Lifecycle responsibilities**: call `AssetPackManager.shared.checkForUpdates()` after OS upgrades and `remove(assetPackWithID:)` when you're done with a pack — the system does NOT auto-evict.
|
||||
@@ -466,7 +467,7 @@ The mock server runs HTTPS only — plain HTTP is not supported by the framework
|
||||
| Read file | `AssetPackManager.shared.contents(at:searchingInAssetPackWithID:options:)` or `.descriptor(for:...)` |
|
||||
| Force update check | `AssetPackManager.shared.checkForUpdates()` |
|
||||
| Remove pack | `AssetPackManager.shared.remove(assetPackWithID:)` |
|
||||
| Localized packs `OS27` | `manifest` `language` tag, `resolvedLanguage`, `reconcilePreferredLanguages()`, `contents(at:asLocalizedFor:options:)` — see `skills/background-assets-ref.md` |
|
||||
| Localized packs `OS27` | `manifest` `language` tag, `resolvedLanguage`, `reconcilePreferredLanguages()`, `contents(at:asLocalizedFor:options:)`; the extension side of a language change arrives as `BAContentRequest.languageChange` — see `skills/background-assets-ref.md` |
|
||||
|
||||
### Tooling
|
||||
|
||||
@@ -502,6 +503,4 @@ For full type signatures, all Info.plist keys, and the unmanaged (legacy) `BADow
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-11
|
||||
**Platforms**: iOS 26+, iPadOS 26+, macOS 26+, tvOS 26+, visionOS 26+ (managed); iOS 16.1+ (unmanaged legacy)
|
||||
**Status**: Phase A — discipline file; reference and TDD pressure-test pass to follow
|
||||
**Platforms**: OS26 for managed assets, not watchOS; iOS 16.1+ for unmanaged legacy
|
||||
|
||||
@@ -477,5 +477,4 @@ Missing any step = issue at that stage.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-31
|
||||
**Platforms**: iOS 13+
|
||||
|
||||
@@ -777,5 +777,4 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTask
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-31
|
||||
**Platforms**: iOS 13+, iOS 26+ (BGContinuedProcessingTask; watchOS 27 extends it to the watch)
|
||||
|
||||
@@ -1012,6 +1012,4 @@ subsystem:com.apple.backgroundtaskscheduler
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-31
|
||||
**Platforms**: iOS 13+, iOS 26+ (BGContinuedProcessingTask)
|
||||
**Status**: Production-ready background task patterns
|
||||
|
||||
@@ -935,7 +935,6 @@ Button(action: { /* This won't work in widgets */ }) {
|
||||
|
||||
**Timeline entries out of order**: Ensure entry dates are chronological. Use incrementing offsets from `Date()`.
|
||||
|
||||
|
||||
## Performance Issues
|
||||
|
||||
**Symptoms**: Widget rendering slow, battery drain
|
||||
@@ -1004,4 +1003,4 @@ let entries = (0..<100).map { offset in
|
||||
|
||||
---
|
||||
|
||||
**Version**: 0.9 | **Platforms**: iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, visionOS 2+
|
||||
**Platforms**: iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, visionOS 2+
|
||||
|
||||
@@ -60,7 +60,7 @@ Subscriptions can now be sold to groups (a customer buys multiple seats and shar
|
||||
|
||||
- **Requires StoreKit 2**; available for all auto-renewable subscriptions.
|
||||
- **On by default** for most new and existing StoreKit 2 subscriptions. If your subscription has Family Sharing enabled, group/organization sales are **opted out by default** so you control how the two options interact.
|
||||
- For group purchases, your own in-app UI triggers the StoreKit 2 purchase flow: get the number of seats requested from the customer and pass it into the StoreKit 2 purchase request. **No dedicated purchase-option symbol for the seat count ships as of the Xcode 27 beta 6 SDK** (only the general `.quantity(_:)` option) — re-check later betas before writing code against this.
|
||||
- For group purchases, your own in-app UI triggers the StoreKit 2 purchase flow: get the number of seats requested from the customer and pass it into the StoreKit 2 purchase request. **No dedicated purchase-option symbol for the seat count ships as of the Xcode 27.0 SDK** (only the general `.quantity(_:)` option) — check newer SDKs before writing code against this.
|
||||
- When seat assignments complete (either purchase type), **the App Store assigns a transaction for each member** — your existing `Transaction.updates` / entitlement flow grants access per member. Member transactions surface as `Transaction.OwnershipType.assigned` (`"ASSIGNED"`); seat revocations as `Transaction.revocationType == .assignmentRevocation` (`"ASSIGNMENT_REVOKE"`; `revocationType` is a 26.4 field); the storefront platform for managed distribution appears as `AppStore.Platform.managed`. All three ship back-deployed in the 27 SDK.
|
||||
- By default group purchases use Apple's included seat management (invite-link generation, acceptance tracking, seat lifecycle such as cancellations). Custom invitation flows will be powered by new App Store Server API endpoints (not yet named).
|
||||
- App Store Server API **Group management endpoints** let you query all the groups a customer is in and all the members in a group — supported for volume purchasing and for group purchases using the included seat management flows.
|
||||
@@ -399,7 +399,7 @@ if let offer = transaction.offer {
|
||||
|
||||
#### From WWDC 2025-241:8:00
|
||||
|
||||
Server-signed payloads can additionally carry `offerType: 5` (retention offer, WWDC 2026) — no client `OfferType` case exists for it as of the 27 beta 6 SDK; see Retention Messaging API below.
|
||||
Server-signed payloads can additionally carry `offerType: 5` (retention offer, WWDC 2026) — no client `OfferType` case exists for it as of the 27.0 RC SDK (`.winBack` = 4 is still the last); see Retention Messaging API below.
|
||||
|
||||
### Current Entitlements
|
||||
|
||||
@@ -1257,7 +1257,7 @@ let result = try await AppStore.presentOfferCodeRedeemSheet(
|
||||
)
|
||||
```
|
||||
|
||||
`RedeemOption` is `Equatable` / `Hashable` / `Sendable`, but **no public option values ship as of the Xcode 27 beta 6 SDK** — pass `[]` (the UIKit/AppKit variants default it) and re-check later betas for concrete options. Not available on tvOS or watchOS.
|
||||
`RedeemOption` is `Equatable` / `Hashable` / `Sendable`, but **no public option values ship as of the Xcode 27.0 RC SDK** — pass `[]` (the UIKit/AppKit variants default it) and re-check 27.x SDKs for concrete options. Not available on tvOS or watchOS.
|
||||
|
||||
### Payment Mode
|
||||
|
||||
@@ -1463,7 +1463,7 @@ Seat revocations from group/volume purchasing surface as `ASSIGNMENT_REVOKE` —
|
||||
|
||||
Retention messages appear in the subscription cancellation flow. The ASC-configuration side (views, message/image rules, retention offers, save-rate guidance) lives in `axiom-shipping (skills/app-store-ref.md)` Part 11; this is the server surface.
|
||||
|
||||
**Retention offers in signed payloads** — redeeming a retention offer surfaces as a new `offerType` value of `5` in the signed transaction and renewal info, with the usual offer fields (`offerIdentifier`, `offerDiscountType`, `offerPeriod`) populated as expected. There is no corresponding `Transaction.OfferType` case as of the Xcode 27 beta 6 SDK (`.winBack`, raw value 4, is the last named case) — match the raw value server-side.
|
||||
**Retention offers in signed payloads** — redeeming a retention offer surfaces as a new `offerType` value of `5` in the signed transaction and renewal info, with the usual offer fields (`offerIdentifier`, `offerDiscountType`, `offerPeriod`) populated as expected. There is no corresponding `Transaction.OfferType` case as of the Xcode 27.0 SDK (`.winBack`, raw value 4, is the last named case) — match the raw value server-side.
|
||||
|
||||
**Real-time Retention Messaging** — your server answers a server-to-server HTTP request from the App Store at cancellation time. Requires passing a sandbox performance test before production, and access is granted via an interest form. The Retention Messaging API lives at `https://api.storekit.apple.com/inApps/v1/messaging`:
|
||||
|
||||
|
||||
Generated
+2
-1
@@ -16,11 +16,12 @@ description: "Use when building ANY macOS app — windows, menus, sandboxing, di
|
||||
| Menu bar, commands, keyboard shortcuts | See `skills/menus-and-commands.md` |
|
||||
| Table, Inspector, NavigationSplitView, focus | See `skills/swiftui-differences.md` |
|
||||
| App Sandbox, file access, security-scoped bookmarks | See `skills/sandbox-and-file-access.md` |
|
||||
| Another team's app or app group container access denied without a prompt (`OS27`) | See `skills/sandbox-and-file-access.md` |
|
||||
| Developer ID, notarization, Sparkle auto-updates | See `skills/direct-distribution.md` |
|
||||
| NSViewRepresentable, NSHostingController, AppKit bridging, @Observable in AppKit, NSHostingMenu, SwiftUI scenes from AppKit | See `skills/appkit-interop.md` |
|
||||
| Modernizing AppKit: mouseDown replacement, control events, status-item sessions, state restoration, concentric corners, touch (`OS27`) | See `skills/appkit-modernization.md` |
|
||||
| Screen recording, sharing, or capture (ScreenCaptureKit) | See `skills/screencapturekit.md` |
|
||||
| SCStream / SCContentFilter / screenshots / file recording API | See `skills/screencapturekit-ref.md` |
|
||||
| SCStream / SCContentFilter / screenshots / file recording API, clip buffering, recording editor (`OS27`) | See `skills/screencapturekit-ref.md` |
|
||||
| Bring an iOS app to Mac: Catalyst adoption, Designed for iPad, Mac availability opt-out | See `skills/ios-apps-on-mac.md` |
|
||||
| Apple Pay on Mac / Catalyst | See `axiom-payments/skills/apple-pay.md` (Catalyst section) |
|
||||
|
||||
|
||||
@@ -328,6 +328,12 @@ Any process with access to the parent document can resolve these bookmarks.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Team Container Access `OS27`
|
||||
|
||||
macOS 27 no longer prompts when an app accesses files in another developer team's **app data containers or app group containers**. Such access is denied by default, and the user manages exceptions in Privacy & Security settings (release note 161835690). No compiler diagnostic or SDK annotation flags it.
|
||||
|
||||
Code that reads or writes another team's container now fails at runtime with no prompt. An App Group does not route around this, because the release note covers other teams' group containers too. Sign both apps under one team and share an App Group, or have the user allow the access in Privacy & Security.
|
||||
|
||||
## Entitlements
|
||||
|
||||
### Core Sandbox Entitlement
|
||||
|
||||
+112
-8
@@ -13,6 +13,30 @@ Comprehensive API reference for ScreenCaptureKit: content enumeration, filtering
|
||||
- **SCContentSharingPicker** — System selection UI that hands back an `SCContentFilter` (macOS 14+).
|
||||
- **SCScreenshotManager** — One-shot frame capture (macOS 14+).
|
||||
- **SCRecordingOutput** — Records a stream straight to a file (macOS 15+).
|
||||
- **SCClipBufferingOutput** — Rolling ≤15 s buffer exported on demand as instant-replay clips.
|
||||
- **SCRecordingEditor** — System preview/share UI for a finished recording.
|
||||
|
||||
---
|
||||
|
||||
## Platform availability `OS27`
|
||||
|
||||
ScreenCaptureKit is no longer macOS-only — it ships on iOS 27, iPadOS 27, tvOS 27, and visionOS 27. **This reference documents the macOS surface.** The iOS model differs in kind: `SCShareableContent`, `SCDisplay`, `SCWindow`, and every `SCContentFilter` initializer are unavailable there, so the system picker is the only way to obtain a filter. For iOS and iPadOS see axiom-media (skills/screen-capture.md).
|
||||
|
||||
The 27-cycle additions, and where each one actually exists:
|
||||
|
||||
| Added in the 27 cycle | Platforms |
|
||||
|---|---|
|
||||
| `SCClipBufferingOutput`, `SCStream.addClipBufferingOutput(_:)` / `removeClipBufferingOutput(_:)` | all |
|
||||
| `SCRecordingEditor` — presenter differs: `NSWindow` on macOS, `UIWindowScene` elsewhere | all |
|
||||
| `SCStream.isCapturing`, `SCContentSharingPicker.isAvailable` | all |
|
||||
| `SCRecordingOutputConfiguration.mixesAudioWithMicrophone` | all |
|
||||
| `SCStreamError.Code.insufficientStorage`, `.notSupported` | all |
|
||||
| `SCStreamError.Code.missingBackgroundMode` | all — but it carries no `macos` clause, so on macOS it inherits the enum's 12.3 floor and needs **no** `@available(macOS 27, *)` guard; background modes are an iOS-family concept, so expect it from iOS/tvOS/visionOS |
|
||||
| `SCContentFilter.isMicrophoneEnabled`, `SCStreamFrameInfo.videoOrientation` | all except tvOS |
|
||||
| `SCContentSharingPicker.presentForCurrentApplication()`, `SCContentSharingPickerConfiguration.showsMicrophoneControl` | **not macOS** — iOS and visionOS, plus tvOS for the first |
|
||||
| `SCVideoEffectOutput`, `addVideoEffectOutput(_:)` / `removeVideoEffectOutput(_:)`, `SCStreamDelegate.outputVideoEffectDidFail(for:withError:)`, `SCContentFilter.isCameraEnabled`, `SCContentSharingPickerConfiguration.showsCameraControl` | **iOS only** |
|
||||
|
||||
The last two rows are hard compile errors on macOS, not soft runtime failures: `'presentForCurrentApplication()' is unavailable in macOS`.
|
||||
|
||||
---
|
||||
|
||||
@@ -49,10 +73,13 @@ SCContentFilter(display: display, including: [window1, window2])
|
||||
filter.contentRect // CGRect of captured content
|
||||
filter.pointPixelScale // Float backing scale
|
||||
filter.streamType // SCStreamType
|
||||
filter.isMicrophoneEnabled // OS27 — read-only, reflects the picker's mic control
|
||||
```
|
||||
|
||||
Audio is filtered only at the application level, never per-window.
|
||||
|
||||
`isMicrophoneEnabled` is readable on macOS, but the control that sets it — `SCContentSharingPickerConfiguration.showsMicrophoneControl` — is iOS/visionOS-only. Unavailable on tvOS.
|
||||
|
||||
---
|
||||
|
||||
# Part 3: Stream configuration (SCStreamConfiguration)
|
||||
@@ -97,6 +124,7 @@ try stream.addStreamOutput(output, type: .microphone, sampleHandlerQueue: micQue
|
||||
|
||||
try await stream.startCapture()
|
||||
try await stream.stopCapture()
|
||||
stream.isCapturing // OS27 — started and actively capturing
|
||||
|
||||
// Hot updates — no restart
|
||||
try await stream.updateConfiguration(newConfig)
|
||||
@@ -114,8 +142,8 @@ func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuff
|
||||
|
||||
```swift
|
||||
func stream(_ stream: SCStream, didStopWithError error: Error)
|
||||
func outputEffectDidStart(for stream: SCStream) // Presenter Overlay began (macOS 14+)
|
||||
func outputEffectDidStop(for stream: SCStream)
|
||||
func outputVideoEffectDidStart(for stream: SCStream) // Presenter Overlay began (macOS 14+)
|
||||
func outputVideoEffectDidStop(for stream: SCStream)
|
||||
```
|
||||
|
||||
## Frame attachments (SCStreamFrameInfo)
|
||||
@@ -123,11 +151,12 @@ func outputEffectDidStop(for stream: SCStream)
|
||||
```swift
|
||||
let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false)
|
||||
as? [[SCStreamFrameInfo: Any]]
|
||||
// keys: .status, .displayTime, .scaleFactor, .contentRect, .dirtyRects, .contentScale
|
||||
// keys: .status, .displayTime, .scaleFactor, .contentRect, .dirtyRects, .contentScale,
|
||||
// .videoOrientation (OS27, not tvOS)
|
||||
// status value is an SCFrameStatus: .complete, .idle, .blank, .suspended, .started, .stopped
|
||||
```
|
||||
|
||||
Use only `.complete` frames; `.idle` carries no new IOSurface.
|
||||
Use only `.complete` frames; `.idle` carries no new IOSurface. The `.videoOrientation` value follows the `CGImagePropertyOrientation` enum.
|
||||
|
||||
---
|
||||
|
||||
@@ -138,6 +167,7 @@ let picker = SCContentSharingPicker.shared
|
||||
picker.add(observer) // SCContentSharingPickerObserver
|
||||
picker.isActive = true
|
||||
picker.maximumStreamCount = 1
|
||||
picker.isAvailable // OS27 — is screen recording allowed on this device?
|
||||
|
||||
var config = SCContentSharingPickerConfiguration()
|
||||
config.allowedPickerModes = [.singleWindow, .multipleWindows, .singleApplication,
|
||||
@@ -182,6 +212,7 @@ let recConfig = SCRecordingOutputConfiguration()
|
||||
recConfig.outputURL = url
|
||||
recConfig.outputFileType = .mov // AVFileType
|
||||
recConfig.videoCodecType = .h264 // AVVideoCodecType
|
||||
recConfig.mixesAudioWithMicrophone = false // OS27 — false keeps system + mic as two tracks (default true)
|
||||
|
||||
let recording = SCRecordingOutput(configuration: recConfig, delegate: recDelegate)
|
||||
try stream.addRecordingOutput(recording)
|
||||
@@ -199,11 +230,84 @@ func recordingOutputDidFinishRecording(_ recordingOutput: SCRecordingOutput)
|
||||
|
||||
---
|
||||
|
||||
# Part 8: Permissions and migration
|
||||
# Part 8: Clip buffering (SCClipBufferingOutput) `OS27`
|
||||
|
||||
A rolling buffer of the most recent samples — **15 seconds maximum** — exported on demand as instant-replay clips. Buffering continues during an export.
|
||||
|
||||
```swift
|
||||
let clips = SCClipBufferingOutput(delegate: self)
|
||||
try stream.addClipBufferingOutput(clips) // stream must already be capturing
|
||||
try await clips.exportClip(to: url, duration: 15) // newest samples; 15 s max
|
||||
try stream.removeClipBufferingOutput(clips) // stops buffering, flushes the buffer
|
||||
```
|
||||
|
||||
- Add only while the stream is **actively capturing**; one clip-buffering session per stream.
|
||||
- A `duration` longer than what is buffered yields all available content, not an error.
|
||||
- The destination URL must be a file URL, and is overwritten if it already exists.
|
||||
- `exportClip(to:duration:completionHandler:)` is the non-async form.
|
||||
- Stopping the stream stops buffering; removing the output discards everything buffered.
|
||||
- What gets buffered follows the stream's `SCStreamConfiguration`.
|
||||
|
||||
## SCClipBufferingOutputDelegate
|
||||
|
||||
```swift
|
||||
extension Recorder: SCClipBufferingOutputDelegate {
|
||||
func clipBufferingOutputDidStartBuffering(_ clipBufferingOutput: SCClipBufferingOutput) {}
|
||||
func clipBufferingOutput(_ clipBufferingOutput: SCClipBufferingOutput, didFailWithError error: any Error) {}
|
||||
func clipBufferingOutputDidStopBuffering(_ clipBufferingOutput: SCClipBufferingOutput) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Part 9: Recording editor (SCRecordingEditor) `OS27`
|
||||
|
||||
System-owned preview and share UI for a finished recording — the file written by `SCRecordingOutput` or exported by `SCClipBufferingOutput`. The editor owns its entire presentation lifecycle.
|
||||
|
||||
```swift
|
||||
let editor = SCRecordingEditor(url: url) // file written by SCRecordingOutput or SCClipBufferingOutput
|
||||
editor.delegate = self
|
||||
try await editor.present(from: window) // NSWindow; present(from:completionHandler:) also exists
|
||||
```
|
||||
|
||||
On macOS the presenter takes an `NSWindow`; on iOS, iPadOS, visionOS, and tvOS it takes a `UIWindowScene`. There is no anchor-less overload — `present()` fails with `error: missing argument for parameter 'from' in call`. Apple's discussion describes automatic foreground discovery, but every `present` overload in the 27 SDK requires an anchor. The `SCRecordingEditor.Mode` preview-versus-share variant is **tvOS-only** — the macOS `present(from:mode:completionHandler:)` overload is explicitly unavailable.
|
||||
|
||||
## SCRecordingEditorDelegate
|
||||
|
||||
```swift
|
||||
extension Recorder: SCRecordingEditorDelegate {
|
||||
func recordingEditorDidDismiss(_ editor: SCRecordingEditor) {}
|
||||
func recordingEditor(_ editor: SCRecordingEditor, didFailWithError error: any Error) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Part 10: Errors (SCStreamError)
|
||||
|
||||
Failures arrive as `NSError` in `SCStreamErrorDomain`; match them against `SCStreamError.Code`.
|
||||
|
||||
```swift
|
||||
do { try await stream.startCapture() }
|
||||
catch SCStreamError.userDeclined { } // user declined to authorize capture
|
||||
catch SCStreamError.insufficientStorage { } // OS27 — recording ran out of storage
|
||||
catch SCStreamError.notSupported { } // OS27 — operation unsupported on this platform
|
||||
catch { } // other NSError in SCStreamErrorDomain
|
||||
```
|
||||
|
||||
| New in the 27 cycle | Raw | Meaning |
|
||||
|---|---|---|
|
||||
| `.insufficientStorage` | -3822 | stream stopped — not enough storage for the recording |
|
||||
| `.notSupported` | -3823 | the operation is unsupported on this platform |
|
||||
| `.missingBackgroundMode` | -3824 | required background mode not configured. Carries **no `macos` clause**, so on macOS it inherits the enum's 12.3 floor — no `@available` guard needed there; background modes are an iOS-family concept, so expect it from iOS/tvOS/visionOS |
|
||||
|
||||
---
|
||||
|
||||
# Part 11: Permissions and migration
|
||||
|
||||
- **Screen Recording TCC** is mandatory; `SCShareableContent` is empty until granted.
|
||||
- **Persistent Content Capture** entitlement for login-item/background capturers (VNC, remote desktop).
|
||||
- Presenter Overlay is automatic for any ScreenCaptureKit + camera app; observe `outputEffectDidStart`.
|
||||
- Presenter Overlay is automatic for any ScreenCaptureKit + camera app; observe `outputVideoEffectDidStart`.
|
||||
|
||||
| Deprecated | Replacement |
|
||||
|------------|-------------|
|
||||
@@ -217,6 +321,6 @@ func recordingOutputDidFinishRecording(_ recordingOutput: SCRecordingOutput)
|
||||
|
||||
**WWDC**: 2022-10156, 2022-10155, 2023-10136, 2024-10088
|
||||
|
||||
**Docs**: /screencapturekit, /screencapturekit/scshareablecontent, /screencapturekit/sccontentfilter, /screencapturekit/scstreamconfiguration, /screencapturekit/scstream, /screencapturekit/scstreamoutput, /screencapturekit/sccontentsharingpicker, /screencapturekit/scscreenshotmanager, /screencapturekit/screcordingoutput
|
||||
**Docs**: /screencapturekit, /screencapturekit/scshareablecontent, /screencapturekit/sccontentfilter, /screencapturekit/scstreamconfiguration, /screencapturekit/scstream, /screencapturekit/scstreamoutput, /screencapturekit/sccontentsharingpicker, /screencapturekit/scscreenshotmanager, /screencapturekit/screcordingoutput, /screencapturekit/scclipbufferingoutput, /screencapturekit/screcordingeditor, /screencapturekit/scstreamerror
|
||||
|
||||
**Skills**: skills/screencapturekit.md, skills/sandbox-and-file-access.md, axiom-media (ReplayKit, CMSampleBuffer), axiom-concurrency (serial queues, async)
|
||||
**Skills**: skills/screencapturekit.md, skills/sandbox-and-file-access.md, axiom-media (skills/screen-capture.md for iOS/iPadOS, ReplayKit, CMSampleBuffer), axiom-concurrency (serial queues, async)
|
||||
|
||||
+6
-6
@@ -22,17 +22,17 @@ Filters and configurations can be swapped **on the fly** without tearing down th
|
||||
- Recording screen content straight to a file (macOS 15+)
|
||||
- Migrating off `CGDisplayStream` / `CGWindowListCreateImage` / `AVCaptureScreenInput`
|
||||
|
||||
This is **macOS only**. For iOS screen capture, use ReplayKit (`RPScreenRecorder` / broadcast extensions) — see axiom-media. For the full type/property surface, see `skills/screencapturekit-ref.md`. For sandbox/entitlement details, see `skills/sandbox-and-file-access.md`.
|
||||
This file documents the **macOS** surface. ScreenCaptureKit is **no longer macOS-only** — it ships on iOS 27, iPadOS 27, tvOS 27, and visionOS 27 — but the iOS model differs in kind: no `SCShareableContent`, and none of the `SCContentFilter` initializers, so the picker is the entry point. For that, see axiom-media (`skills/screen-capture.md`); ReplayKit (`RPScreenRecorder` / broadcast extensions) remains the path below iOS 27. For the full type/property surface, see `skills/screencapturekit-ref.md`. For sandbox/entitlement details, see `skills/sandbox-and-file-access.md`.
|
||||
|
||||
## System Requirements
|
||||
|
||||
| API | Availability |
|
||||
|-----|--------------|
|
||||
| `SCStream`, `SCShareableContent`, `SCContentFilter`, `SCStreamConfiguration`, `SCStreamOutput` | macOS |
|
||||
| `SCContentSharingPicker`, `SCScreenshotManager`, Presenter Overlay (`outputEffectDidStart`) | macOS |
|
||||
| `SCContentSharingPicker`, `SCScreenshotManager`, Presenter Overlay (`outputVideoEffectDidStart`) | macOS |
|
||||
| `SCRecordingOutput`, microphone capture (`captureMicrophone`), HDR (`captureDynamicRange`) | macOS 15.0+ |
|
||||
| Mac Catalyst | 18.2+ |
|
||||
| iOS / iPadOS | **Not available — use ReplayKit** |
|
||||
| iOS / iPadOS | **iOS 27+**, picker-based capture only — see axiom-media (`skills/screen-capture.md`). Use ReplayKit below 27 |
|
||||
|
||||
Screen capture requires the user's **Screen Recording** permission (TCC). Without it, `SCShareableContent` returns no shareable content. A background/login-item capturer (VNC, remote desktop) additionally needs the **Persistent Content Capture** entitlement.
|
||||
|
||||
@@ -40,7 +40,7 @@ Screen capture requires the user's **Screen Recording** permission (TCC). Withou
|
||||
|
||||
| Gotcha | Why it bites | Fix |
|
||||
|--------|--------------|-----|
|
||||
| Building for iOS | ScreenCaptureKit is macOS-only | Use ReplayKit on iOS |
|
||||
| Porting this file's code to iOS | iOS 27 ships ScreenCaptureKit but without `SCShareableContent` or the `SCContentFilter` initializers | Drive capture from `SCContentSharingPicker` on iOS 27+; ReplayKit below 27 |
|
||||
| No frames ever arrive | Screen Recording permission not granted | `SCShareableContent` is empty without TCC consent — request it and handle the empty case |
|
||||
| UI hitches / dropped frames | Heavy work on the sample-handler queue | Pass a dedicated **serial** `DispatchQueue`; copy what you need and return fast |
|
||||
| Memory balloons or the stream stalls | Holding IOSurface-backed buffers past `queueDepth` | Process and release each `CMSampleBuffer` promptly; tune `queueDepth` |
|
||||
@@ -177,7 +177,7 @@ try await stream.startCapture()
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- Shipping screen capture on iOS — that's ReplayKit, not ScreenCaptureKit.
|
||||
- Assuming ScreenCaptureKit is still macOS-only — it ships on iOS 27+, with a picker-first API shape rather than this file's enumerate-then-filter model.
|
||||
- Not handling the empty `SCShareableContent` case when Screen Recording permission is denied.
|
||||
- Doing real work (encoding, disk I/O, UI updates) directly on the sample-handler queue.
|
||||
- Retaining IOSurface-backed video buffers — exhausts the pool and stalls capture.
|
||||
@@ -191,4 +191,4 @@ try await stream.startCapture()
|
||||
|
||||
**Docs**: /screencapturekit, /screencapturekit/scstream, /screencapturekit/scshareablecontent, /screencapturekit/sccontentfilter, /screencapturekit/scstreamconfiguration, /screencapturekit/sccontentsharingpicker, /screencapturekit/scscreenshotmanager, /screencapturekit/screcordingoutput
|
||||
|
||||
**Skills**: skills/screencapturekit-ref.md, skills/sandbox-and-file-access.md (TCC, entitlements), axiom-media (ReplayKit for iOS, CMSampleBuffer handling), axiom-concurrency (async sequences, serial queues)
|
||||
**Skills**: skills/screencapturekit-ref.md, skills/sandbox-and-file-access.md (TCC, entitlements), axiom-media (skills/screen-capture.md — iOS 27 ScreenCaptureKit, ReplayKit below 27, CMSampleBuffer handling), axiom-concurrency (async sequences, serial queues)
|
||||
|
||||
Generated
+10
-5
@@ -19,12 +19,14 @@ Delegate to the appropriate Cursor subagent when this router calls for a special
|
||||
| Camera capture, AVCaptureSession | See `skills/camera-capture.md` |
|
||||
| Slow camera launch / deferred start (iOS 26+), ProRes recording via Pro Video Storage (`OS27`) | See `skills/camera-capture.md` Patterns 8-9 |
|
||||
| Camera API (RotationCoordinator, etc.) | See `skills/camera-capture-ref.md` |
|
||||
| Lens aperture / shutter / ISO priority modes, exposure signals, continuous autofocus tracking, low-light video noise reduction, cinematic video metadata capture (`OS27`) | See `skills/camera-capture-ref.md` |
|
||||
| Center Stage front camera (iPhone 17), dynamic aspect ratio, smart framing, 24/48 MP capture | See `skills/camera-capture-ref.md` |
|
||||
| iPhone Duo front cameras (virtual front camera, inner/outer ultra-wide), camera direction vs position | See `skills/camera-capture.md` Pattern 6 and `skills/camera-capture-ref.md` (iPhone Duo Front Cameras) |
|
||||
| Camera freezes, black preview, rotation | See `skills/camera-capture-diag.md` |
|
||||
| Photo pickers, library access | See `skills/photo-library.md` |
|
||||
| PHPicker, PhotosPicker API reference | See `skills/photo-library-ref.md` |
|
||||
| PHPicker, PhotosPicker, PhotoKit API reference: asset metadata editing (keywords, rating, caption), persistent change observer, background resource upload (`OS27`) | See `skills/photo-library-ref.md` |
|
||||
| PhotoKit + Swift 6 strict concurrency, `nonisolated` observer, `performChanges` isolation, `_dispatch_assert_queue_fail` | See axiom-concurrency (skills/isolation-inheritance-diag.md) |
|
||||
| Audio, AVFoundation, spatial audio | See `skills/avfoundation-ref.md` |
|
||||
| Audio, AVFoundation, spatial audio, async session activation, throwing AVAudioEngine APIs (`OS27`) | See `skills/avfoundation-ref.md` |
|
||||
| Video write/export/playback, sample-buffer engine, resumable export, Apple Log 2, iOS 27 deprecations (`OS27`) | See `skills/avfoundation-video-ref.md` |
|
||||
| Audio recognition, ShazamKit | See `skills/shazamkit.md` |
|
||||
| ShazamKit API reference | See `skills/shazamkit-ref.md` |
|
||||
@@ -36,7 +38,7 @@ Delegate to the appropriate Cursor subagent when this router calls for a special
|
||||
| Now Playing metadata, remote commands | See `skills/now-playing.md` |
|
||||
| Animated lock-screen artwork (iOS 26+) | See `skills/now-playing.md` Pattern 8 |
|
||||
| NowPlaying framework (`import NowPlaying`, Swift-native `MediaSession`, `OS27`) | See `skills/now-playing.md` (NowPlaying Framework section) |
|
||||
| Cast / route media to non-AirPlay devices (Google Cast/Chromecast, DLNA) as system routes, AVSystemRouting (`iOS27`, EU-gated/beta) | See `skills/system-media-routing.md` |
|
||||
| Cast / route media to non-AirPlay devices (Google Cast/Chromecast, DLNA) as system routes, AVSystemRouting (`iOS27`, likely EU-gated) | See `skills/system-media-routing.md` |
|
||||
| Screen capture / recording / streaming the screen or your own app, ScreenCaptureKit (`OS27` — new on iOS/iPadOS/tvOS/visionOS 27; macOS 12.3+) | See `skills/screen-capture.md` |
|
||||
| CarPlay HIG, app categories, design rules, entitlements | See `skills/carplay-hig.md` |
|
||||
| CarPlay templates reference (all 12 templates, availability matrix, depth limits) | See `skills/carplay-templates-ref.md` |
|
||||
@@ -81,7 +83,7 @@ digraph media {
|
||||
7. Haptics? → `skills/haptics.md`
|
||||
8. Now Playing / remote commands? → `skills/now-playing.md`, `skills/now-playing-carplay.md`, `skills/now-playing-musickit.md`
|
||||
9. Reading the user's Apple Music **library** (enumerate songs/playlists, library identity, sync)? → `skills/music-library.md` — a different problem from playback; read it before any library walk
|
||||
10. Cast / route media to non-AirPlay devices (Google Cast/Chromecast, DLNA) as system routes? → `skills/system-media-routing.md` (`iOS27`, EU-gated/beta)
|
||||
10. Cast / route media to non-AirPlay devices (Google Cast/Chromecast, DLNA) as system routes? → `skills/system-media-routing.md` (`iOS27`, likely EU-gated)
|
||||
11. Screen capture / recording / streaming the screen or your own app (ScreenCaptureKit)? → `skills/screen-capture.md` (`OS27` — new on iOS/iPadOS/tvOS/visionOS 27)
|
||||
12. CarPlay app design, category selection, entitlement request? → `skills/carplay-hig.md` (start here for any CarPlay work)
|
||||
13. DockKit motorized stands / gimbals, subject tracking, custom motor control? → `skills/dockkit.md`
|
||||
@@ -144,7 +146,7 @@ digraph media {
|
||||
| "`Song.id` is a stable key I can store" | Its *format* differs per device for the same library (`i.…` on one, bare numeric on another). Never parse it, never use it as a cross-device key. |
|
||||
| "DockKit is just pairing a stand" | Custom control needs system tracking disabled, handles inverted dock states, and two different coordinate origins. |
|
||||
| "Grouping faces is just Vision face detection" | Vision detects faces in one image; MediaIntelligence clusters them into persistent people (entities) across a whole library, with its own working directory and state. |
|
||||
| "Casting to Chromecast means bundling the Google Cast SDK" | On iOS 27, AVSystemRouting exposes non-AirPlay routes as system routes — you adopt one Apple API (observe events, start a session, drive playbackControl) instead of a per-vendor SDK. Likely EU-gated/beta — gate and keep a fallback. |
|
||||
| "Casting to Chromecast means bundling the Google Cast SDK" | On iOS 27, AVSystemRouting exposes non-AirPlay routes as system routes — you adopt one Apple API (observe events, start a session, drive playbackControl) instead of a per-vendor SDK. Likely EU-gated — gate and keep a fallback. |
|
||||
| "I'll sync SharePlay playback by broadcasting the current time over the messenger" | The messenger has no clock. `AVDelegatingPlaybackCoordinatorPlayCommand.hostClockTime` gives you an absolute `CMClockGetHostTimeClock()` start time; hand-rolling reinvents startup barriers, seek ordering, stalls, and interruptions badly. |
|
||||
| "The playback coordinator handles interruptions for me" | Only `AVPlayerPlaybackCoordinator` does. `AVDelegatingPlaybackCoordinator` adds **no** automatic suspensions — a custom engine begins *and* ends every one, and a suspension never ended hangs the whole group. |
|
||||
| "ScreenCaptureKit on iPad works like the Mac (enumerate displays/windows)" | On iOS/iPadOS `SCShareableContent` and all `SCContentFilter` initializers are macOS-only — you get a filter ONLY from the system `SCContentSharingPicker`. New on iOS/iPadOS/tvOS/visionOS 27. Also not `ImageRenderer` (that snapshots your own SwiftUI view). |
|
||||
@@ -160,6 +162,9 @@ User: "My camera app takes a second before preview appears"
|
||||
User: "Support the Center Stage front camera" / "Capture 48MP photos"
|
||||
→ Read: `skills/camera-capture-ref.md`
|
||||
|
||||
User: "How do I switch cameras on iPhone Duo?" / "Choose a camera by direction instead of position"
|
||||
→ Read: `skills/camera-capture.md` (Pattern 6) and `skills/camera-capture-ref.md` (iPhone Duo Front Cameras)
|
||||
|
||||
User: "Camera freezes when I get a phone call"
|
||||
→ Read: `skills/camera-capture-diag.md`
|
||||
|
||||
|
||||
+179
-2
@@ -37,6 +37,8 @@ try AVAudioSession.sharedInstance().setCategory(
|
||||
)
|
||||
```
|
||||
|
||||
On 27, `engine.connect` and `player.play()` are **deprecated**, while `setActive` merely gains an async alternative (it is not deprecated) — see Async Activation and the Deactivation Model and Throwing Engine and Node APIs below.
|
||||
|
||||
---
|
||||
|
||||
## AVAudioSession
|
||||
@@ -85,6 +87,8 @@ try AVAudioSession.sharedInstance().setCategory(
|
||||
|
||||
### Interruption Handling
|
||||
|
||||
This pattern warns on 27 — `InterruptionType` and `InterruptionOptions` are deprecated. See Async Activation and the Deactivation Model below.
|
||||
|
||||
```swift
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: AVAudioSession.interruptionNotification,
|
||||
@@ -150,6 +154,106 @@ NotificationCenter.default.addObserver(
|
||||
|
||||
---
|
||||
|
||||
## Async Activation and the Deactivation Model OS27
|
||||
|
||||
Unavailable on macOS. This replaces the interruption-notification model for tracking why audio stopped.
|
||||
|
||||
### Async activate and deactivate
|
||||
|
||||
`setActive(_:)` blocks the calling thread. The 27 calls return immediately and deliver the result asynchronously. Both also have `completionHandler:` forms.
|
||||
|
||||
```swift
|
||||
@available(iOS 27, *)
|
||||
func beginPlayback() async throws {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try session.setCategory(.playback)
|
||||
_ = try await session.activate()
|
||||
}
|
||||
|
||||
@available(iOS 27, *)
|
||||
func endPlayback() async throws {
|
||||
_ = try await AVAudioSession.sharedInstance()
|
||||
.deactivate(options: .notifyOthersOnDeactivation)
|
||||
}
|
||||
```
|
||||
|
||||
| API | Introduced | Notes |
|
||||
|---|---|---|
|
||||
| `activate(options:)` | iOS/tvOS/visionOS 27, watchOS 5 | Not new on watchOS. **On watchOS** under the long-form audio routing policy it may present a route picker and fail if the user cancels |
|
||||
| `deactivate(options:)` | 27 | Async alternative to `setActive(false, options:)` (not deprecated) |
|
||||
| `AVAudioSessionDeactivationOptions.notifyOthersOnDeactivation` | 27 | Signals an interrupted app that it may resume |
|
||||
|
||||
### Deactivation and resumption notifications
|
||||
|
||||
| Notification | userInfo key | Payload |
|
||||
|---|---|---|
|
||||
| `AVAudioSession.didBecomeActiveNotification` | none | no payload |
|
||||
| `AVAudioSession.didBecomeInactiveNotification` | `AVAudioSession.deactivationContextKey` | `AVAudioSession.DeactivationContext` |
|
||||
| `AVAudioSession.resumptionRecommendationNotification` | `AVAudioSession.resumptionContextKey` | `AVAudioSession.ResumptionContext` |
|
||||
|
||||
| Type | Members |
|
||||
|---|---|
|
||||
| `DeactivationContext` | `source` (`.app` / `.system`), `interruptionContext` — non-nil only when another app caused the interruption |
|
||||
| `InterruptionContext` | `reason: AVAudioSession.InterruptionReason` |
|
||||
| `ResumptionContext` | `recommendation` (`.shouldResume` / `.shouldNotResume`) |
|
||||
|
||||
```swift
|
||||
@available(iOS 27, *)
|
||||
func audioDidBecomeInactive(_ notification: Notification, player: AVAudioPlayerNode) {
|
||||
guard let context = notification.userInfo?[AVAudioSession.deactivationContextKey]
|
||||
as? AVAudioSession.DeactivationContext else { return }
|
||||
|
||||
if context.source == .system, context.interruptionContext != nil {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Typed notification messages
|
||||
|
||||
The same three notifications ship as `NotificationCenter.Message` types. The typed path fuses source and interruption into one `DeactivationResult` enum, which the userInfo path does not expose.
|
||||
|
||||
```swift
|
||||
@available(iOS 27, *)
|
||||
@MainActor
|
||||
func observeSession() {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
|
||||
_ = NotificationCenter.default.addObserver(
|
||||
of: session,
|
||||
for: AVAudioSession.DidBecomeInactiveMessage.self
|
||||
) { message in
|
||||
switch message.deactivationResult {
|
||||
case .appDeactivated:
|
||||
break
|
||||
case .systemInterruption(let context):
|
||||
_ = context.reason
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = NotificationCenter.default.addObserver(
|
||||
of: session,
|
||||
for: AVAudioSession.ResumptionRecommendationMessage.self
|
||||
) { message in
|
||||
if message.recommendation == .shouldResume {
|
||||
// resume playback
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Legacy interruption API status
|
||||
|
||||
`AVAudioSession.InterruptionType` and `AVAudioSession.InterruptionOptions` are deprecated in 27, directing callers to the notifications above. The Swift `interruptionNotification` constant and its userInfo keys are *not* themselves marked deprecated, but decoding the payload requires the deprecated types — so the legacy handler still builds, warning on `InterruptionType` and `InterruptionOptions`.
|
||||
|
||||
### New port
|
||||
|
||||
`AVAudioSession.Port.mediaDeviceExtension` — iOS 27 only, unavailable on watchOS, tvOS, visionOS, and macOS. Output to a media device vended through a user-installed system-wide extension.
|
||||
|
||||
---
|
||||
|
||||
## AVAudioEngine
|
||||
|
||||
### Basic Pipeline
|
||||
@@ -224,6 +328,8 @@ inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, time
|
||||
inputNode.removeTap(onBus: 0)
|
||||
```
|
||||
|
||||
`installTap(onBus:bufferSize:format:block:)` is deprecated in 27 — see Throwing Engine and Node APIs below.
|
||||
|
||||
### Format Conversion
|
||||
|
||||
```swift
|
||||
@@ -255,6 +361,74 @@ converter.convert(to: outputBuffer, error: &error) { inNumPackets, outStatus in
|
||||
|
||||
---
|
||||
|
||||
## Throwing Engine and Node APIs OS27
|
||||
|
||||
27 replaces the AVAudioEngine surface that trapped on misuse with throwing equivalents. The originals are deprecated, so existing code keeps building with warnings.
|
||||
|
||||
| Deprecated in 27 | Replacement |
|
||||
|---|---|
|
||||
| `connect(_:to:format:)` | `connectNode(_:to:format:) throws` |
|
||||
| `connect(_:to:fromBus:toBus:format:)` | `connectNode(_:to:fromBus:toBus:format:) throws` |
|
||||
| `connect(_:to:fromBus:format:)` (to `[AVAudioConnectionPoint]`) | `connectNode(_:to:fromBus:format:) throws` |
|
||||
| `play()` | `playAudio() throws` |
|
||||
| `play(at:)` | `playAudio(at:) throws` |
|
||||
| `installTap(onBus:bufferSize:format:block:)` | `installAudioTap(onBus:bufferSize:format:tapProvider:) throws` |
|
||||
| `connectMIDI(_:to:format:eventListBlock:)` | `connectMIDI(_:to:format:eventListProvider:)` — the block type changes with it, `AUMIDIEventListBlock?` → `AVMIDIEventListBlock?`, so a straight rename hits a type error |
|
||||
| `AVAudioFormat.init(cmAudioFormatDescription:)` | `AVAudioFormat.init(formatDescription:)`, now failable |
|
||||
|
||||
A format mismatch that used to crash the process now throws.
|
||||
|
||||
```swift
|
||||
@available(iOS 27, *)
|
||||
func buildGraph(engine: AVAudioEngine, player: AVAudioPlayerNode, format: AVAudioFormat) throws {
|
||||
engine.attach(player)
|
||||
try engine.connectNode(player, to: engine.mainMixerNode, format: format)
|
||||
try engine.start()
|
||||
try player.playAudio()
|
||||
}
|
||||
```
|
||||
|
||||
### Taps deliver a read-only buffer
|
||||
|
||||
`installAudioTap` hands the callback an `AVReadOnlyAudioPCMBuffer` and takes a `@Sendable` closure. `channelData(_:)` returns a `Span`-carrying enum instead of a raw pointer.
|
||||
|
||||
```swift
|
||||
@available(iOS 27, *)
|
||||
func meterInput(engine: AVAudioEngine) throws {
|
||||
let input = engine.inputNode
|
||||
let format = input.outputFormat(forBus: 0)
|
||||
|
||||
try input.installAudioTap(onBus: 0, bufferSize: 1024, format: format) { buffer, time in
|
||||
guard case .float(let samples) = buffer.channelData(0) else { return }
|
||||
var sum: Float = 0
|
||||
for i in 0..<samples.count {
|
||||
sum += samples[i] * samples[i]
|
||||
}
|
||||
let rms = (sum / Float(samples.count)).squareRoot()
|
||||
_ = 20 * log10(rms)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`AVAudioPCMBuffer` gains matching accessors in 27 — `channelData(_:)`, `mutableChannelData(_:)`, `init(copying:)`, `withUnsafeAudioBufferList(_:)` — but not on identical terms: both of its channel-data accessors are marked `@unsafe`, whereas `AVReadOnlyAudioPCMBuffer.channelData(_:)` is not.
|
||||
|
||||
### Realtime-safe render blocks are ObjC only
|
||||
|
||||
All four are marked `__SWIFT_UNAVAILABLE_MSG("Swift is not supported for use with audio realtime threads")` and do not appear in Swift at all. Write realtime render and receive callbacks in ObjC or C.
|
||||
|
||||
| ObjC selector | Type |
|
||||
|---|---|
|
||||
| `initWithRealtimeSafeRenderBlock:` | `AVAudioSourceNode` |
|
||||
| `initWithFormat:realtimeSafeRenderBlock:` | `AVAudioSourceNode` |
|
||||
| `initWithRealtimeSafeReceiverBlock:` | `AVAudioSinkNode` |
|
||||
| `setRealtimeSafeManualRenderingInputPCMFormat:inputBlock:` | `AVAudioInputNode` |
|
||||
|
||||
### Reverb preset
|
||||
|
||||
`AVAudioUnitReverbPreset.outdoorGeneral` — new in 27, unavailable on watchOS.
|
||||
|
||||
---
|
||||
|
||||
## Bit-Perfect Audio / DAC Output
|
||||
|
||||
### iOS Behavior
|
||||
@@ -587,6 +761,8 @@ NotificationCenter.default.addObserver(
|
||||
)
|
||||
```
|
||||
|
||||
On 27, observe `didBecomeInactiveNotification` and `resumptionRecommendationNotification` instead.
|
||||
|
||||
### Tap Memory Leaks
|
||||
|
||||
```swift
|
||||
@@ -609,6 +785,8 @@ engine.connect(playerNode, to: mixerNode, format: wrongFormat) // Crash!
|
||||
engine.connect(playerNode, to: mixerNode, format: nil)
|
||||
```
|
||||
|
||||
On 27, prefer `connectNode(_:to:format:)`, which throws instead of trapping.
|
||||
|
||||
### Forgetting to Activate Session
|
||||
|
||||
```swift
|
||||
@@ -631,6 +809,5 @@ try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
---
|
||||
|
||||
**Targets:** iOS 12+ (core), iOS 26+ (spatial features)
|
||||
**Targets:** iOS 12+ (core), iOS 26+ (spatial features), iOS 27 (async activation, throwing engine APIs)
|
||||
**Frameworks:** AVFoundation, AVKit, Cinematic (iOS 26+)
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -246,6 +246,8 @@ let cameras = discoverySession.devices
|
||||
| `.builtInTrueDepthCamera` | Front TrueDepth (Face ID) |
|
||||
| `.builtInLiDARDepthCamera` | LiDAR depth |
|
||||
|
||||
iPhone Duo adds inner and outer ultra-wide front cameras and a virtual front camera; the individual cameras' device types are announced for the iOS 27.1 SDK — see iPhone Duo Front Cameras below.
|
||||
|
||||
### Device Configuration
|
||||
|
||||
```swift
|
||||
@@ -276,6 +278,79 @@ do {
|
||||
}
|
||||
```
|
||||
|
||||
### Lens Aperture and Exposure Priority Modes `OS27`
|
||||
|
||||
iOS / Mac Catalyst / tvOS 27 — **not macOS, visionOS, or watchOS**. `AVCaptureDevice.lensAperture` (iOS 8) stays **read-only** in 27; what 27 adds is a setter *method* for devices with an adjustable diaphragm.
|
||||
|
||||
```swift
|
||||
let format = device.activeFormat
|
||||
format.minLensAperture // Float 𝑓-numbers
|
||||
format.maxLensAperture
|
||||
format.defaultLensAperture
|
||||
format.recommendedLensApertureStops // [Float], sorted — ONE element means the aperture is fixed
|
||||
|
||||
// Not every combination is supported — validate first
|
||||
guard format.supportsExposureModeCustom(lensAperture: 2.8,
|
||||
duration: CMTime(value: 1, timescale: 120),
|
||||
iso: 400) else { return }
|
||||
|
||||
try device.lockForConfiguration()
|
||||
defer { device.unlockForConfiguration() }
|
||||
let syncTime = await device.setExposureModeCustom(lensAperture: 2.8,
|
||||
duration: CMTime(value: 1, timescale: 120),
|
||||
iso: 400)
|
||||
```
|
||||
|
||||
**Priority modes** come from passing a sentinel for whatever the system should keep adjusting:
|
||||
|
||||
| Sentinel | Effect |
|
||||
|-----|------|
|
||||
| `AVCaptureDevice.autoLensAperture` | System drives the aperture |
|
||||
| `AVCaptureDevice.autoExposureDuration` | System drives the exposure duration |
|
||||
| `AVCaptureDevice.autoISO` | System drives the gain |
|
||||
| `AVCaptureDevice.currentLensAperture` | Lock at the current aperture — the device may be mid-adjustment, so this can differ from a `lensAperture` you just read |
|
||||
|
||||
Aperture priority is `(2.8, .autoExposureDuration, .autoISO)`; shutter priority is `(.autoLensAperture, duration, .autoISO)`. The duration and ISO sentinels also work with the older `setExposureModeCustom(duration:iso:)`.
|
||||
|
||||
Read back what the system is driving — `automaticallyAdjustsLensAperture`, `automaticallyAdjustsExposureDuration`, `automaticallyAdjustsISO`, all read-only.
|
||||
|
||||
Two behaviors worth knowing before you tune: `autoExposureLensApertureRateLimit` smooths aperture motion while auto-exposure holds any parameter, but a fully explicit call (no sentinels) applies aperture changes immediately with no rate limit; and `activeMaxExposureDuration` caps streaming frames when **either aperture or ISO** is Auto, while a call with no Auto parameter ignores it and applies your duration as specified.
|
||||
|
||||
### Exposure Signals `OS27`
|
||||
|
||||
Scene characteristics the auto-exposure system may react to. iOS / Mac Catalyst / tvOS 27.
|
||||
|
||||
| Signal | What auto-exposure may do |
|
||||
|-----|------|
|
||||
| `.subjectMotion` | Close the aperture or shorten exposure to cut motion blur |
|
||||
| `.groupPhoto` | Close the aperture for more depth of field when several faces are present |
|
||||
| `.document` | Close the aperture to sharpen textual scenes |
|
||||
| `.starburst` | Open the aperture to remove diffraction artifacts from point light sources |
|
||||
| `.flicker` | Adjust the aperture so exposure duration can avoid artificial-lighting frequencies |
|
||||
|
||||
`supportedExposureSignals` is what the device allows; `activeExposureSignals` (KVO) is what auto-exposure currently associates with the scene.
|
||||
|
||||
`enabledExposureSignals` is assignable **only** while `automaticallyEnablesExposureSignals` is `false` — assigning under the default `true` throws `NSInvalidArgumentException`, and assigning without `lockForConfiguration()` throws `NSGenericException`.
|
||||
|
||||
### Continuous Autofocus Tracking `OS27`
|
||||
|
||||
macOS / iOS / Mac Catalyst / tvOS 27 — not visionOS or watchOS. Keeps the subject at `focusPointOfInterest` in focus as it moves through the scene.
|
||||
|
||||
**Subscribing the metadata output is a precondition, not a nicety.** The header is explicit: unless the device is connected to an `AVCaptureMetadataOutput` subscribing `.focusTrackedObject`, no tracking updates are delivered at all and `isContinuousAutoFocusTrackingSubjectAcquired` stays `false`.
|
||||
|
||||
```swift
|
||||
metadataOutput.metadataObjectTypes = [.focusTrackedObject] // required, or nothing tracks
|
||||
|
||||
guard device.activeFormat.isContinuousAutoFocusTrackingSupported else { return }
|
||||
try device.lockForConfiguration()
|
||||
device.isContinuousAutoFocusTrackingEnabled = true
|
||||
device.continuousAutoFocusTrackingLensPositionBias = 0 // -1 nearest … 0 median depth … 1 furthest
|
||||
device.focusMode = .continuousAutoFocus // assign LAST — this is what engages tracking
|
||||
device.unlockForConfiguration()
|
||||
```
|
||||
|
||||
Order matters twice over: assigning `focusMode` is the engage trigger, and a later bias change takes effect only if you assign `focusMode = .continuousAutoFocus` **again** after it. `isContinuousAutoFocusTrackingSubjectAcquired` (KVO) reports *whether* a subject is tracked, not which one — read the `AVMetadataFocusTrackedObject`s from the metadata output to identify it.
|
||||
|
||||
### Switching Cameras
|
||||
|
||||
```swift
|
||||
@@ -304,6 +379,8 @@ func switchCamera() {
|
||||
|
||||
**Important**: Always switch on the session queue, within beginConfiguration/commitConfiguration.
|
||||
|
||||
**iPhone Duo**: `position` doesn't say which way a camera faces — see iPhone Duo Front Cameras.
|
||||
|
||||
### Authorization
|
||||
|
||||
```swift
|
||||
@@ -422,6 +499,21 @@ connection.preferredVideoStabilizationMode = .lowLatency
|
||||
|
||||
Bonus (iOS 26+, iOS-only): `device.nominalFocalLengthIn35mmFilm` — nominal 35mm-equivalent focal length (`0` for virtual/external devices).
|
||||
|
||||
### iPhone Duo Front Cameras — Announced for the iOS 27.1 SDK
|
||||
|
||||
From Apple's tech talk 111465. **Absent from the 27.0 SDK; announced for 27.1, and names can change before it ships. Check the installed SDK first (`xcrun --sdk iphoneos --show-sdk-version`). Below 27.1: don't write these in code as if they compile — describe them, name the talk, and use the virtual front camera (existing discovery API) today. On 27.1 or later, betas included: grep the SDK's `.swiftinterface` and headers for the declaration; if it's there, the SDK's spelling and signature win over this table; if it's missing, say it was renamed or dropped. A filename or `#import` hit isn't a declaration — `AVKit/AVCaptureDeviceDirectionCoordinator.h` ships in 27.0 as an empty stub. Don't call them fictional. Don't fill in parameters, types, or cases this table doesn't give.** Spellings follow the talk's code where the narration differs.
|
||||
|
||||
iPhone Duo has two front cameras, both square-sensor ultra-wides: one on the outer display and an under-display camera on the inner one. Direction replaces position as the question to ask — a `.front` camera can face away from the user, and a rear camera faces the user when the device is flipped open.
|
||||
|
||||
| Key | API | Behavior | Talk |
|
||||
|---|---|---|---|
|
||||
| `camera.virtual` | (no type name given) | Discovered by a discovery session for `.front` with the wide-angle or ultra-wide type; switches automatically — inner camera when open, outer when closed, "the most relevant front camera for your app"; the talk doesn't say which it picks when flipped open with the app on the outer display — don't assume either; only shared capabilities — up to 1080p and 60 fps, no depth | 111465 0:57 |
|
||||
| `camera.types` | `.builtInOuterUltraWideCamera`, `.builtInInnerUltraWideCamera` | Individual cameras with full capabilities — outer up to 4K and up to 120 fps, inner 1080p up to 60 fps; depth only here; your app switches on open and close | 111465 1:44 |
|
||||
| `camera.direction` | `AVCaptureDeviceDirectionCoordinator(view:deviceTypes:changeHandler:)` (AVKit) | Reports which cameras face toward and away from the user relative to one view, and calls the handler when that view's display changes — as the device opens or closes, or as the app moves to the outer display while flipped open. Main-actor; one per view, so two when showing UI on both displays | 111465 4:06 |
|
||||
| `camera.descriptor` | `AVCaptureDeviceDescriptor` | Main-actor-safe, Sendable stand-in for an `AVCaptureDevice`; the coordinator provides descriptors rather than devices (the sample's handler receives a value named `map` — its type isn't shown). Pass them to your camera actor and reconfigure the session there — don't call AVFoundation from the handler | 111465 5:38 |
|
||||
|
||||
When the handler fires: hand the forward-facing camera's descriptor to your camera actor, which reconfigures the session and applies mirroring (mirror when a rear camera faces the user), and update UI from the handler (the coordinator is main-actor). To override automatic mirroring, the actor sets the preview connection's `automaticallyAdjustsVideoMirroring = false` before setting `isVideoMirrored`, and only when `isVideoMirroringSupported`, or AVFoundation throws `NSInvalidArgumentException`. From then on the app owns mirroring on every switch: `isVideoMirrored = true` whenever the camera in use faces the user, `false` when it faces away. Today's APIs that still apply on Duo: `videoGravity` to fit or fill the preview on the inner display (a full-field-of-view rear-camera stream leaves room around it for controls), `setDynamicAspectRatio(_:)` for a landscape crop from the square sensor (see Dynamic Aspect Ratio above), the rotation coordinator (it updates when the app changes displays), and sensor-orientation compensation — on by default for every Duo front camera; disable it once you apply the rotation coordinator's capture angle (see Sensor Orientation Compensation above). Apple articles: "Choosing a Camera by the Direction it Faces", "Supporting Device Rotation in Your Camera App".
|
||||
|
||||
---
|
||||
|
||||
## AVCaptureDevice.RotationCoordinator (iOS 17+)
|
||||
@@ -838,6 +930,52 @@ extension CameraManager: AVCaptureFileOutputRecordingDelegate {
|
||||
}
|
||||
```
|
||||
|
||||
### Low-Light Video Noise Reduction `OS27`
|
||||
|
||||
Lives on `AVCaptureConnection`, not the session. macOS / iOS / Mac Catalyst / tvOS / visionOS 27, not watchOS.
|
||||
|
||||
| API | Notes |
|
||||
|-----|-------|
|
||||
| `AVCaptureDevice.Format.isLowLightVideoNoiseReductionSupported` | Format-level capability |
|
||||
| `AVCaptureConnection.isLowLightVideoNoiseReductionSupported` | KVO. Reflects the *active* configuration — flips as active format, video stabilization mode, auto video frame rate, or max frame rate change |
|
||||
| `automaticallyEnablesLowLightVideoNoiseReduction` | Defaults to `true` on movie file output connections. Buys quality with power |
|
||||
| `isLowLightVideoNoiseReductionEnabled` | Settable only after the automatic flag is `false`, and only when supported — either violation throws `NSInvalidArgumentException` |
|
||||
|
||||
### Cinematic Video Metadata `OS27`
|
||||
|
||||
Records a metadata track alongside video so the Cinematic framework can apply cinematic (rack-focus) editing after capture. macOS / iOS / Mac Catalyst / tvOS 27, not visionOS or watchOS.
|
||||
|
||||
| API | Notes |
|
||||
|-----|-------|
|
||||
| `AVCaptureDevice.Format.isCinematicVideoMetadataCaptureSupported` | Format-level capability |
|
||||
| `AVCaptureMovieFileOutput.isCinematicVideoMetadataCaptureSupported` | Also requires a 16:9 or 9:16 (or unset) dynamic aspect ratio and spatial video capture off; changes as you switch camera, format, or features |
|
||||
| `automaticallyAdjustsCinematicVideoMetadataCaptureEnabled` | Default `true` — the framework decides, and capture is **not guaranteed** even when supported. Set `false` to control it yourself |
|
||||
| `isCinematicVideoMetadataCaptureEnabled` | Settable only when the automatic flag is `false` and capture is supported — either violation throws `NSInvalidArgumentException` |
|
||||
| `AVMetadataObject.ObjectType.cinematicVideoMetadata` | For an `AVCaptureMetadataOutput` pipeline — delivers `AVMetadataCinematicVideoMetadataObject` (opaque payload in `timedMetadataGroup`) |
|
||||
|
||||
**Editing what you captured** — `import Cinematic`, macOS / iOS 27 (capability and status checks are also tvOS):
|
||||
|
||||
```swift
|
||||
switch await CNAssetInfo.cinematicCapability(for: asset) { // replaces deprecated isCinematic(asset:)
|
||||
case .renderable:
|
||||
break // ready for CNRenderingSession
|
||||
case .needsPreprocessing:
|
||||
if CNAssetInfo.resourceStatus() == .needsDownloading {
|
||||
try await CNAssetInfo.downloadResources() // device-wide, cached, honors Task cancellation
|
||||
}
|
||||
let info = try await CNAssetInfo(asset: asset)
|
||||
let config = CNAssetPreprocessConfiguration(destinationAssetURL: destinationURL)
|
||||
config.referenceSourceAssetTracks = false // default: copy tracks — portable, ~2x storage
|
||||
_ = try await info.preprocessAsset(configuration: config) // generates the disparity track
|
||||
case .none:
|
||||
break // no cinematic metadata track present
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
`CNResourceStatus` also reports `.unsupportedDevice` and `.unsupportedAsset`; a failed download surfaces as `CNCinematicError.Code.downloadFailed`. Axiom does not otherwise cover Cinematic editing (`CNRenderingSession` and the detection/script APIs); note 27 deprecates that class's `CVPixelBuffer` `encodeRender` overloads in favor of `CVReadOnlyPixelBuffer` ones.
|
||||
|
||||
### Pro Video Storage `OS27`
|
||||
|
||||
Pre-allocated, system-wide storage for high-data-rate captures (e.g. ProRes) giving deterministic file-write performance — normal file I/O is non-deterministic under load (WWDC 2026-303). User controls capacity in Camera settings. Not on visionOS/watchOS.
|
||||
@@ -851,6 +989,7 @@ Pre-allocated, system-wide storage for high-data-rate captures (e.g. ProRes) giv
|
||||
| `openSettings()` | Jump to the Settings allocation UI |
|
||||
| `AVCaptureMovieFileOutput.isProVideoStorageSupported` / `usesProVideoStorage` | Setting the flag while unsupported raises an exception. Recording writes to pre-allocated storage, then moves to your URL when capture finishes |
|
||||
| `AVAssetWriter.isProVideoStorageSupported` / `usesProVideoStorage` | Same pair for `AVCaptureVideoDataOutput`-based recording |
|
||||
| `AVError` `.notEnoughSpaceForProVideoStorageReplenishment` (-11897) | The pool could not be refilled — macOS / iOS / tvOS 27, not visionOS or watchOS |
|
||||
|
||||
Guided adoption flow: camera-capture.md Pattern 9.
|
||||
|
||||
@@ -863,6 +1002,8 @@ Guided adoption flow: camera-capture.md Pattern 9.
|
||||
| `AVCaptureBroadcastVideoOutput` | Broadcast-quality video + ancillary data over the DisplayPort hardware interface (USB-C DP Alt Mode). Delegate reports dropped frames; `maxBufferedFrameCount` (default 0 = drop late frames) vs class `maxSupportedBufferedFrameCount`; `resetFrameBuffer()`; `droppedFrameReplacementPolicy` `.repeatPreviousFrame` (default) / `.blackFrame`; `videoSettings` reports the negotiated SMPTE ST 377 (MXF) format. Verify the format supports it via `AVCaptureDevice.Format.unsupportedCaptureOutputClasses` before adding. Not visionOS/watchOS |
|
||||
| `AVExternalStorageDevice.reasonsNotRecommendedForCaptureUse` | Typed reasons (`.encrypted`, `.unsupportedFileSystem`, `.slowWritingSpeed`, `.unknownWritingSpeed`) replacing the deprecated boolean `isNotRecommendedForCaptureUse` |
|
||||
| External-sync `AVError` cases (iOS only) | `.followExternalSyncFailed` (-11894), `.externalSyncDeviceFrequencyHigherThanSpecified` (-11895), `.externalSyncDeviceFrequencyLowerThanSpecified` (-11896) for the iOS 26 `AVExternalSyncDevice` frame-sync feature |
|
||||
| `AVCaptureBroadcastVideoOutput.ancillaryDataEncoder` | Vends an `AVCaptureAncillaryDataEncoder` (macOS / iOS / Mac Catalyst / tvOS 27) that sends per-frame lens, camera, and user-defined acquisition data with the broadcast video. `isEnabled` toggles encoding; `setRDD18AncillaryData(_:forTag:)` / `setRDD18AncillaryDataString(_:forTag:)` (SMPTE RDD 18 tags 0xE011–0xFFFF) and `setUserInstanceUID(_:forUserUDAMVersion:)` write the user portion; `currentUserDefinedAncillaryData` reads it back keyed by `AVCaptureAncillaryDataUserKey` (`.rdd18InstanceUID`, `.rdd18UDAMSetVersion`, `.rdd18UserItems`); `userDefinedAncillaryDataSizeRemaining` is the remaining byte budget |
|
||||
| `AVCaptureDevice.setPrimaryConstituentDeviceSwitchingBehaviorLockedWith(_:)` | Pins a virtual camera to one constituent device (macOS / iOS / Mac Catalyst / tvOS 27). Gate on `isPrimaryConstituentDeviceSwitchingBehaviorLockedWithDeviceSupported` — calling it unsupported throws `NSInvalidArgumentException` — and hold `lockForConfiguration()`. A `videoZoomFactor` outside the locked device's range is clamped to the nearest supported value |
|
||||
|
||||
---
|
||||
|
||||
@@ -1005,6 +1146,8 @@ extension CameraManager: AVCapturePhotoCaptureDelegate {
|
||||
|
||||
**WWDC**: 2023-10105, 2026-303, 2026-304, 2026-341
|
||||
|
||||
**Tech Talks**: 111465
|
||||
|
||||
**Docs**: /avfoundation/avcapturesession, /avfoundation/avcapturedevice, /avfoundation/avcapturephotosettings, /avfoundation/avcapturedevice/rotationcoordinator, /avfoundation/avprovideostorage, /avfoundation/avcapturesmartframingmonitor
|
||||
|
||||
**Skills**: skills/camera-capture.md, skills/camera-capture-diag.md, skills/avfoundation-video-ref.md (typed buffer attachments, `OS27`)
|
||||
|
||||
@@ -14,6 +14,7 @@ Use when you need to:
|
||||
- ☑ Make camera launch fast — preview up in half the time (deferred start, iOS 26+)
|
||||
- ☑ Handle session interruptions (phone calls, multitasking)
|
||||
- ☑ Switch between front/back cameras
|
||||
- ☑ Switch cameras on iPhone Duo by direction, not position (two front cameras)
|
||||
- ☑ Configure capture quality and resolution (incl. 24/48 MP — see camera-capture-ref)
|
||||
- ☑ Support the Center Stage front camera (iPhone 17 — see camera-capture-ref)
|
||||
- ☑ Record high-data-rate video (ProRes) without dropped frames (Pro Video Storage `OS27`)
|
||||
@@ -31,6 +32,7 @@ Use when you need to:
|
||||
"How do I support the Center Stage front camera?"
|
||||
"My ProRes recording drops frames"
|
||||
"How do I switch between front and back cameras?"
|
||||
"How do I choose the right camera on iPhone Duo?"
|
||||
"How do I record video with audio?"
|
||||
|
||||
## Red Flags
|
||||
@@ -47,6 +49,7 @@ Signs you're making this harder than it needs to be:
|
||||
- ❌ Ignoring `photoQualityPrioritization` (slow captures)
|
||||
- ❌ Not handling `.notAuthorized` permission state
|
||||
- ❌ Modifying session without `beginConfiguration()`/`commitConfiguration()`
|
||||
- ❌ Assigning a 27-cycle capture property while its `automatically…` flag is still `true` (raises an ObjC exception — see Anti-Pattern 5)
|
||||
- ❌ Using UIImagePickerController for custom camera UI (limited control)
|
||||
|
||||
## Mandatory First Steps
|
||||
@@ -291,6 +294,8 @@ func capturePhoto() {
|
||||
}
|
||||
```
|
||||
|
||||
**iPhone Duo**: The rotation coordinator updates when your app moves between displays — keep applying its angles on every change. Once you apply the capture angle, disable sensor-orientation compensation for performance; every Duo front camera enables it by default: `if #available(iOS 26, *) { photoOutput.isCameraSensorOrientationCompensationEnabled = false }` (skills/camera-capture-ref.md, Sensor Orientation Compensation).
|
||||
|
||||
**Cost**: 45 min implementation, prevents 2+ hours debugging rotation issues
|
||||
|
||||
### Pattern 4: Responsive Capture Pipeline (iOS 17+)
|
||||
@@ -635,6 +640,12 @@ func switchCamera() {
|
||||
|
||||
**Front camera mirroring**: Front camera preview is mirrored by default (matches user expectation). Captured photos are NOT mirrored (correct for sharing). This is intentional.
|
||||
|
||||
**iPhone Duo (two front cameras)**:
|
||||
- A discovery session for `.front` with the wide-angle or ultra-wide type finds the virtual front camera, which switches automatically: inner camera when open, outer when closed — Apple says it uses "the most relevant front camera for your app". The talk doesn't say which camera it picks when the device is flipped open with your app on the outer display; don't claim it stays on the inner camera or moves to the outer one — test that pose, or use the 27.1 direction API. It offers only what both cameras share — up to 1080p and 60 fps, no depth. Apple describes discovery, not `default(_:for:position:)`, so use a discovery session.
|
||||
- An individual camera gives its full capabilities, but your app then owns the switch on open and close.
|
||||
- The toggle above flips `position`, which on Duo doesn't say which way a camera faces: a `.front` camera can face away, and flipped open with your app on the outer display, a rear camera and the outer front camera both face the user. Toggling by direction needs the 27.1 API in skills/camera-capture-ref.md (iPhone Duo Front Cameras); on the 27.0 SDK, the virtual front camera with this toggle is the available path.
|
||||
- To show UI on the outer display while the camera runs on the inner one, see axiom-swiftui (skills/iphone-duo.md, Scenes and Accessories).
|
||||
|
||||
**Cost**: 20 min implementation
|
||||
|
||||
### Pattern 7: Video Recording
|
||||
@@ -864,6 +875,24 @@ session.commitConfiguration() // Atomic change
|
||||
|
||||
**Why it matters**: Without configuration block, session may enter invalid state between calls.
|
||||
|
||||
### Anti-Pattern 5: Manual Override While the Automatic Flag Is On `OS27`
|
||||
|
||||
**Wrong**:
|
||||
```swift
|
||||
try device.lockForConfiguration()
|
||||
device.enabledExposureSignals = [.document] // automaticallyEnablesExposureSignals defaults to true
|
||||
```
|
||||
|
||||
**Right**:
|
||||
```swift
|
||||
try device.lockForConfiguration()
|
||||
defer { device.unlockForConfiguration() }
|
||||
device.automaticallyEnablesExposureSignals = false
|
||||
device.enabledExposureSignals = [.document]
|
||||
```
|
||||
|
||||
**Why it matters**: the 27-cycle capture controls ship in pairs — an `automatically…` flag that defaults to letting the system decide, plus the value itself. Assigning the value while the flag is on raises `NSInvalidArgumentException`, and an ObjC exception is not a Swift error: `try`/`catch` does not catch it, so the app terminates. The same pair shape governs `isLowLightVideoNoiseReductionEnabled` (`automaticallyEnablesLowLightVideoNoiseReduction`, on the connection) and `isCinematicVideoMetadataCaptureEnabled` (`automaticallyAdjustsCinematicVideoMetadataCaptureEnabled`, on the movie file output). Assigning `enabledExposureSignals` without `lockForConfiguration()` raises `NSGenericException` for the same reason.
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
### Scenario 1: "Just Make the Camera Work by Friday"
|
||||
@@ -911,6 +940,8 @@ session.commitConfiguration() // Atomic change
|
||||
|
||||
**Push-back template**: "This is intentional Apple behavior. The preview is mirrored like a mirror so users can frame themselves, but the captured photo is unmirrored so text reads correctly when shared. We can add optional mirroring in post-processing if our use case requires it."
|
||||
|
||||
**iPhone Duo**: "Front" no longer means "facing the user". Flipped open, a rear camera can face the user — mirror its preview so it behaves like a selfie camera (talk 111465, 6:05); captured photos stay unmirrored, as above. To override automatic mirroring, set the preview connection's `automaticallyAdjustsVideoMirroring = false` before setting `isVideoMirrored`, and only when `isVideoMirroringSupported` — otherwise AVFoundation throws `NSInvalidArgumentException`. From then on your app owns mirroring on every camera change: set `isVideoMirrored` to `true` whenever the camera in use faces the user (front or rear) and to `false` when it faces away. Make these connection changes where you reconfigure the session (camera actor or session queue).
|
||||
|
||||
## Checklist
|
||||
|
||||
Before shipping camera features:
|
||||
@@ -957,6 +988,16 @@ Before shipping camera features:
|
||||
- ☑ Switch happens on session queue
|
||||
- ☑ Fallback if new camera unavailable
|
||||
|
||||
**iPhone Duo** (if supported):
|
||||
- ☑ Open/close camera switch handled — by the virtual front camera, or by your own switch for individual cameras
|
||||
- ☑ User-facing camera chosen by direction (27.1 API) or through the virtual front camera — never by `position` alone
|
||||
- ☑ Preview mirrored when a rear camera faces the user and unmirrored when it faces away
|
||||
- ☑ Rotation angles re-applied when the app changes displays
|
||||
- ☑ Sensor-orientation compensation disabled once the capture angle is applied (iOS 26+)
|
||||
- ☑ Switching between individual cameras creates a new rotation coordinator for the new device
|
||||
- ☑ Direction changes reconfigure the session on the camera actor or session queue, never on the main actor
|
||||
- ☑ Tested closed, open, and flipped open with the app on the outer display
|
||||
|
||||
**Video Recording** (if applicable):
|
||||
- ☑ Microphone input added
|
||||
- ☑ Recording delegate handles completion
|
||||
@@ -966,6 +1007,8 @@ Before shipping camera features:
|
||||
|
||||
**WWDC**: 2021-10247, 2023-10105, 2026-303, 2026-304, 2026-341
|
||||
|
||||
**Tech Talks**: 111465
|
||||
|
||||
**Docs**: /avfoundation/avcapturesession, /avfoundation/avcapturedevice/rotationcoordinator, /avfoundation/avcapturephotosettings, /avfoundation/avcapturephotooutputreadinesscoordinator, /avfoundation/avprovideostorage
|
||||
|
||||
**Skills**: skills/camera-capture-ref.md, skills/camera-capture-diag.md, skills/photo-library.md
|
||||
|
||||
@@ -1267,9 +1267,3 @@ NowPlaying is the recommended path on 27. Apple's WWDC session does **not** depr
|
||||
**Docs**: /nowplaying, /nowplaying/publishing-media-sessions, /mediaplayer/mpnowplayinginfocenter, /mediaplayer/mpremotecommandcenter, /mediaplayer/mpnowplayingsession, /mediaplayer/mpmediaitemanimatedartwork, /mediaplayer/providing-animated-artwork-for-media-items
|
||||
|
||||
**Skills**: skills/avfoundation-ref.md, skills/now-playing-carplay.md, skills/now-playing-musickit.md
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-01-04
|
||||
**Status**: iOS 18+ discipline skill covering Now Playing, CarPlay, and MusicKit integration
|
||||
**Tested**: Based on WWDC 2019-501, WWDC 2022-110338 patterns
|
||||
|
||||
@@ -288,6 +288,27 @@ Completion types: creation → `PHSharedAlbumCreationResult?`; posting → `Resu
|
||||
|
||||
Deprecated in 27: `postToPhotosSharedAlbumSheet` (iOS 26.0) → `photosSharedAlbumPostingSheet`. Two breaks: `photoLibrary:` and `defaultAlbumIdentifier:` swap order, and the completion type changes `Result<Void, _>` → `Result<String, _>`.
|
||||
|
||||
### Apple Reference Image `OS27`
|
||||
|
||||
iOS/macOS 27 only — unavailable on tvOS/watchOS/visionOS. Detects and displays images carrying Apple Reference Image data (HEIC, JPEG, or DNG on disk, or a library asset).
|
||||
|
||||
```swift
|
||||
let info = PHReferenceImageInfo(asset: asset) // or PHReferenceImageInfo(fileURL:)
|
||||
if await Task.detached(operation: { info.imageContainsReferenceImageData }).value { selection = asset }
|
||||
|
||||
// Non-nil binding presents the viewer; the closure delivers the processing result
|
||||
.photosReferenceImageViewer(asset: $selection) { (result: Result<PHAsset, any Error>) in }
|
||||
```
|
||||
|
||||
| Modifier | Binding | Completion |
|
||||
|---|---|---|
|
||||
| `photosReferenceImageViewer(asset:onProcessingCompletion:)` | `Binding<PHAsset?>` | `Result<PHAsset, any Error>` |
|
||||
| `photosReferenceImageViewer(pickerItem:onProcessingCompletion:)` | `Binding<PhotosPickerItem?>` | `Result<PHAsset, any Error>` |
|
||||
| `photosReferenceImageViewer(pickerResult:onProcessingCompletion:)` | `Binding<PHPickerResult?>` | `Result<PHAsset, any Error>` |
|
||||
| `photosReferenceImageViewer(fileURL:onProcessingCompletion:)` | `Binding<URL?>` | `Result<URL, any Error>` |
|
||||
|
||||
`imageContainsReferenceImageData` reads image metadata on **first access** — Apple's own guidance is to wrap it in a `Task` rather than block the main thread. Hide any "show reference image" affordance when it returns `false`; it also always returns `false` for iPad apps on visionOS.
|
||||
|
||||
### Loading Images from PhotosPickerItem
|
||||
|
||||
```swift
|
||||
@@ -433,6 +454,53 @@ final class PhotoObserver: NSObject, PHPhotoLibraryChangeObserver {
|
||||
}
|
||||
```
|
||||
|
||||
### Persistent Change History Observer `OS27`
|
||||
|
||||
`PHPhotoLibraryChangeObserver` (above) reports changes only while your process is running. The change-history API (`fetchPersistentChanges(since:)`, iOS 16) survives launches but had to be polled; the 27 observer pushes a notification when history advances. Requires read-write authorization.
|
||||
|
||||
**Registration imports as `register(_:)` — an overload of the `PHPhotoLibraryChangeObserver` one.** Only the unregister keeps a distinct name, `unregisterPersistentChangesObserver(_:)`. A type conforming to **both** protocols cannot just call `register(self)` twice — it converts to both existentials, so the call fails outright with `error: ambiguous use of 'register'`. Disambiguate at the call site: `register(self as any PHPhotoLibraryPersistentChangesObserver)`, and `as any PHPhotoLibraryChangeObserver` for the in-process channel.
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
@MainActor
|
||||
final class LibraryHistoryObserver: NSObject, PHPhotoLibraryPersistentChangesObserver {
|
||||
private var token: PHPersistentChangeToken
|
||||
|
||||
override init() {
|
||||
token = PHPhotoLibrary.shared().currentChangeToken
|
||||
super.init()
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
}
|
||||
|
||||
deinit { PHPhotoLibrary.shared().unregisterPersistentChangesObserver(self) }
|
||||
|
||||
nonisolated func photoLibraryPersistentChangesDidUpdate(_ photoLibrary: PHPhotoLibrary) {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
for change in try photoLibrary.fetchPersistentChanges(since: self.token) {
|
||||
self.token = change.changeToken
|
||||
}
|
||||
} catch PHPhotosError.persistentChangeTokenExpired {
|
||||
self.token = photoLibrary.currentChangeToken
|
||||
} catch {
|
||||
assertionFailure("fetchPersistentChanges failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Gotcha | Consequence |
|
||||
|---|---|
|
||||
| Observer is held **weakly** | register a long-lived object or callbacks stop silently |
|
||||
| Callback runs on an arbitrary serial queue | same `nonisolated` + hop rule as `photoLibraryDidChange` above |
|
||||
| `PHPersistentChangeFetchResult` is refined into a Swift `Sequence` | iterate with `for`-`in`; `enumerateChanges` does not exist in Swift |
|
||||
| Token not advanced to each `change.changeToken` | the same history replays forever |
|
||||
| Token the library has dropped | throws `PHPhotosError.persistentChangeTokenExpired` |
|
||||
|
||||
An expired token leaves no delta to apply. Apple documents the condition but prescribes no recovery — `PHError`'s comment says only that the token "refers to a library state that is older than the available history of persistent changes" — so treat the remedy as the sound consequence rather than stated behavior: re-baseline from `currentChangeToken` and refetch what you display. Persist the token across launches — that is the entire point of this API over the in-process observer.
|
||||
|
||||
---
|
||||
|
||||
## Sendability
|
||||
@@ -519,6 +587,97 @@ let assets = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: ni
|
||||
|
||||
---
|
||||
|
||||
## Asset Metadata Editing `OS27`
|
||||
|
||||
iOS/iPadOS/macOS/tvOS/visionOS 27. Photos ships no watchOS slice at all — there is no `Photos.framework` in the watchOS 27 SDK, so `import Photos` fails there outright and no availability guard changes that.
|
||||
|
||||
Keywords, rating, Live Photo playback, and the RAW/compressed original choice are **change-request** writes: they exist only on `PHAssetChangeRequest` and take effect only inside `performChanges`.
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
func editAsset(_ asset: PHAsset) async throws {
|
||||
try await PHPhotoLibrary.shared().performChanges { @Sendable in
|
||||
let request = PHAssetChangeRequest(for: asset)
|
||||
request.addKeyword("vacation")
|
||||
request.removeKeyword("draft")
|
||||
request.rating = .four
|
||||
request.setLivePhotoVideoPlaybackEnabled(false)
|
||||
request.revertAssetContent(to: .raw)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| API | Where it lives | Notes |
|
||||
|---|---|---|
|
||||
| `addKeyword(_:)` / `removeKeyword(_:)` | `PHAssetChangeRequest` | adding a keyword already present, or removing one absent, is silently ignored |
|
||||
| `rating` | `PHAssetChangeRequest` (settable), `PHAsset` (read) | `PHAsset.Rating`: `.unset`, `.one` … `.five` |
|
||||
| `caption` | `PHAssetChangeRequest` (settable), `PHAssetExtendedMetadata` (read) | writable in 27 — `nil` or an empty string clears it |
|
||||
| `setLivePhotoVideoPlaybackEnabled(_:)` | `PHAssetChangeRequest` | `false` makes a Live Photo present as a still; Live Photos only |
|
||||
| `revertAssetContent(to:)` | `PHAssetChangeRequest` | RAW+JPEG only; unsupported on other asset types |
|
||||
| `originalResourceChoice` | `PHAsset` (read), `PHAssetCreationRequest` and `PHContentEditingInputRequestOptions` (settable) | **not** on `PHAssetChangeRequest` — the compiler rejects it there |
|
||||
|
||||
`revertAssetContent(to:)` is not `revertAssetContentToOriginal()` plus a flag. Beyond reverting adjustments it selects which original — `.raw` or `.compressed` — becomes the unadjusted base for every later render. Apple states the local-originals requirement on the older `revertAssetContentToOriginal()`: download originals with `PHAssetResourceManager` before either call.
|
||||
|
||||
Reading keywords back needs `PHAssetExtendedMetadata` (`OS27`). `keywords` is **not** a `PHAsset` property:
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
func read(_ asset: PHAsset) -> ([String], String?, PHAsset.Rating, PHAsset.OriginalResourceChoice) {
|
||||
let meta = asset.extendedMetadata
|
||||
return (meta.keywords, meta.caption, asset.rating, asset.originalResourceChoice)
|
||||
}
|
||||
```
|
||||
|
||||
`extendedMetadata` (`caption`, `originalFilename`, `keywords`) fetches on demand for each asset. For a grid or any batch, prefetch it in the same fetch instead of paying that per row:
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
func prefetch() -> PHFetchResult<PHAsset> {
|
||||
let options = PHFetchOptions()
|
||||
options.prefetchAssetExtendedMetadata = true
|
||||
return PHAsset.fetchAssets(with: .image, options: options)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collections and Identifiers `OS27`
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
func rootFolder() -> PHCollectionList? {
|
||||
PHCollectionList.fetchCollectionLists(with: .folder, subtype: .rootFolder, options: nil).firstObject
|
||||
}
|
||||
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
func topLevel() -> PHFetchResult<PHCollection> {
|
||||
PHCollection.fetchTopLevelUserCollections(with: nil)
|
||||
}
|
||||
```
|
||||
|
||||
There is always exactly one root folder, and it refuses rename and delete. Fetching its *contents* is equivalent to the long-standing `PHCollection.fetchTopLevelUserCollections(with:)` — what the subtype adds is the list **object**, to hand to `fetchCollectionsInCollectionList(_:options:)` or to recognize the root while walking a hierarchy.
|
||||
|
||||
`localIdentifierMappings(forSynced:)` resolves cloud identifiers using **iCloud-synced records only**:
|
||||
|
||||
```swift
|
||||
@available(anyAppleOS 27, *)
|
||||
@available(watchOS, unavailable)
|
||||
func resolveSynced(_ ids: [PHCloudIdentifier]) -> [String] {
|
||||
let mappings: [PHCloudIdentifier: Result<String, any Error>] =
|
||||
PHPhotoLibrary.shared().localIdentifierMappings(forSynced: ids)
|
||||
return mappings.values.compactMap { try? $0.get() }
|
||||
}
|
||||
```
|
||||
|
||||
The existing `localIdentifierMappings(for:)` falls back to **media-content matching** when a cloud identifier has not synced to this device, which can map one cloud identifier onto several local identifiers (`PHPhotosError.multipleIdentifiersFound`, 3202; the doc comment's `…MultipleLocalIdentifiersFound` does not exist). The 27 method skips that fallback — reach for it when a plausible-but-wrong match is worse than no match. Both are expensive; batch every lookup into one call rather than calling per identifier. The ObjC result wrapper `PHLocalIdentifierMapping` is `NS_REFINED_FOR_SWIFT` and never surfaces in Swift — you get `Result<String, any Error>`.
|
||||
|
||||
---
|
||||
|
||||
## PHAssetCreationRequest
|
||||
|
||||
Create new assets in the photo library.
|
||||
@@ -654,6 +813,45 @@ func exportedID(for resource: PHAssetResource) async throws -> CKAsset.ExportedA
|
||||
- **Apple doc bug (27.0b)**: the doc comment says to gate on `PHAssetResource.TypeGroup.coreComponents`, which does NOT exist anywhere in the 27 SDK. Gate on `PHAssetResourceType` and handle the throw — quoting the doc verbatim yields non-compiling code.
|
||||
- The returned `ExportedAssetID` is device-bound and expires in days — hand it straight to `CKAsset(importing:)`; never persist or transmit it.
|
||||
|
||||
## Background Asset Resource Upload `OS27`
|
||||
|
||||
Host-app control of the `com.apple.photos.background-upload` extension. **iOS/iPadOS/macOS/Mac Catalyst** — explicitly unavailable on tvOS, visionOS, and watchOS, so a visionOS build fails with "is unavailable in visionOS", not a version warning. The APIs below are 27 with one exception: `uploadJobExtensionEnabled` itself dates to iOS 26.1. Requires full library access and the registered extension point.
|
||||
|
||||
```swift
|
||||
@available(iOS 27, macOS 27, macCatalyst 27, *)
|
||||
@available(tvOS, unavailable)
|
||||
@available(visionOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
func configureUploads() throws {
|
||||
let library = PHPhotoLibrary.shared()
|
||||
|
||||
let options = PHAssetResourceUploadJobOptions()
|
||||
options.preventsExpensiveNetworkAccess = true
|
||||
|
||||
try library.enableUploadJobExtension(with: options)
|
||||
_ = library.uploadJobExtensionEnabled
|
||||
_ = library.uploadJobExtensionOptions
|
||||
|
||||
try library.setUploadJobExtensionOptions(PHAssetResourceUploadJobOptions())
|
||||
try library.disableUploadJobExtension()
|
||||
}
|
||||
```
|
||||
|
||||
| API | Notes |
|
||||
|---|---|
|
||||
| `enableUploadJobExtension(with:)` | enables and applies options atomically, as one change; `nil` means defaults |
|
||||
| `setUploadJobExtensionOptions(_:)` | a separate change afterward; pass a fresh `PHAssetResourceUploadJobOptions()` to reset to defaults |
|
||||
| `disableUploadJobExtension()` | |
|
||||
| `uploadJobExtensionOptions` | `nil` when the extension is disabled or the caller is unauthorized |
|
||||
| `preventsExpensiveNetworkAccess` | `false` by default; `true` confines the service to Wi-Fi/Ethernet, never cellular |
|
||||
| `PHAssetResource.assetResource(forUploadJob:)` | returns `nil` if the resource is gone; replaces `PHAssetResourceUploadJob.resource`, which is soft-deprecated on iOS and unavailable on macOS and Mac Catalyst |
|
||||
|
||||
The three configuration calls are ObjC `BOOL` + `NSError**`, so they import as `throws` — there is no `Bool` to test. `enableUploadJobExtension(with:)` supersedes `setUploadJobExtensionEnabled(_:)`, deprecated in 27 and iOS-only. On the extension side `PHBackgroundResourceUploadExtension` is likewise deprecated in favor of `PHBackgroundResourceUploadJobExtension` (`processJobs() async` returning `PHBackgroundResourceUploadProcessingResult`, `willTerminate() async`).
|
||||
|
||||
`PHAssetResourceUploadJob` itself is **not** new — it shipped in iOS 26.1. What 27 adds is the macOS/Mac Catalyst port plus the options and lifecycle APIs above.
|
||||
|
||||
---
|
||||
|
||||
## PHFetchResult
|
||||
|
||||
Ordered list of assets from a fetch.
|
||||
|
||||
+6
-7
@@ -1,7 +1,7 @@
|
||||
|
||||
# Screen Capture — ScreenCaptureKit on iOS/iPadOS `OS27`
|
||||
|
||||
`import ScreenCaptureKit` — capture the screen (or your own app) as a live video + audio stream, record it to a file, or buffer recent content for instant-replay clips. **New on iOS 27, iPadOS 27, tvOS 27, visionOS 27** (all beta); macOS has had it since 12.3. This is the modern replacement for ReplayKit-style capture.
|
||||
`import ScreenCaptureKit` — capture the screen (or your own app) as a live video + audio stream, record it to a file, or buffer recent content for instant-replay clips. **New on iOS 27, iPadOS 27, tvOS 27, visionOS 27**; macOS has had it since 12.3. This is the modern replacement for ReplayKit-style capture.
|
||||
|
||||
> **The iOS model is NOT the macOS model.** On macOS you enumerate `SCShareableContent` (displays/windows/apps) and build an `SCContentFilter` programmatically. **On iOS/iPadOS none of that exists** — `SCShareableContent`, `SCDisplay`, `SCWindow`, `SCRunningApplication`, and every `SCContentFilter` initializer are `API_UNAVAILABLE(ios)`. You get a filter **only** from the system **`SCContentSharingPicker`** (user-driven, privacy-preserving). Writing iOS capture from the macOS mental model will not compile.
|
||||
|
||||
@@ -38,13 +38,12 @@ final class ScreenCapture: NSObject, SCContentSharingPickerObserver, SCStreamDel
|
||||
// The user's choice arrives here as a ready-to-use filter.
|
||||
func contentSharingPicker(_ picker: SCContentSharingPicker,
|
||||
didUpdateWith filter: SCContentFilter, for stream: SCStream?) {
|
||||
Task { await begin(with: filter) }
|
||||
begin(with: filter)
|
||||
}
|
||||
func contentSharingPicker(_ p: SCContentSharingPicker, didCancelFor s: SCStream?) {}
|
||||
func contentSharingPickerStartDidFailWithError(_ error: any Error) {}
|
||||
|
||||
@available(iOS 27, *)
|
||||
func begin(with filter: SCContentFilter) async {
|
||||
func begin(with filter: SCContentFilter) {
|
||||
let config = SCStreamConfiguration()
|
||||
config.width = 1080
|
||||
config.height = 1920
|
||||
@@ -54,7 +53,7 @@ final class ScreenCapture: NSObject, SCContentSharingPickerObserver, SCStreamDel
|
||||
do {
|
||||
// Option A — raw frames as CMSampleBuffers:
|
||||
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global())
|
||||
try await stream.startCapture()
|
||||
stream.startCapture { error in /* SCStreamError, or nil once capturing */ }
|
||||
self.stream = stream
|
||||
} catch { /* SCStreamError */ }
|
||||
}
|
||||
@@ -66,7 +65,7 @@ final class ScreenCapture: NSObject, SCContentSharingPickerObserver, SCStreamDel
|
||||
}
|
||||
```
|
||||
|
||||
`SCContentSharingPicker` essentials: `.shared` (singleton), `.isAvailable`, `.isActive` (must be `true`), `.add(_:)` / `.remove(_:)`, `.present()` (not tvOS), `.presentForCurrentApplication()` (iOS/visionOS/tvOS — captures only your app), `.present(using:)` for a `SCShareableContentStyle`. iOS-only config knobs: `SCContentSharingPickerConfiguration.showsMicrophoneControl` and `.showsCameraControl` (in-app only) — the macOS mode/exclusion options (`allowedPickerModes`, `excludedWindowIDs`, …) are macOS-only.
|
||||
`SCContentSharingPicker` essentials: `.shared` (singleton), `.isAvailable`, `.isActive` (must be `true`), `.add(_:)` / `.remove(_:)`, `.present()` (not tvOS), `.presentForCurrentApplication()` (iOS/visionOS/tvOS — captures only your app), `.present(using:)` for a `SCShareableContentStyle`. Config knobs: `SCContentSharingPickerConfiguration.showsMicrophoneControl` (iOS/visionOS) and `.showsCameraControl` (iOS, in-app only) — the macOS mode/exclusion options (`allowedPickerModes`, `excludedWindowIDs`, …) are macOS-only.
|
||||
|
||||
## Output options (add to the `SCStream`)
|
||||
|
||||
@@ -75,7 +74,7 @@ final class ScreenCapture: NSObject, SCContentSharingPickerObserver, SCStreamDel
|
||||
| Raw frames | `addStreamOutput(_:type:sampleHandlerQueue:)` + `SCStreamOutput` | `CMSampleBuffer`s; `SCStreamOutputType` = `.screen` / `.audio` / `.microphone` |
|
||||
| Record to file | `addRecordingOutput(_:)` with `SCRecordingOutput(configuration:delegate:)` | `SCRecordingOutputConfiguration.outputURL` (+ `videoCodecType` default H.264, `outputFileType` default MPEG-4). Add **before** `startCapture` to catch the first frame |
|
||||
| Instant-replay clips | `addClipBufferingOutput(_:)` | rolling ~15 s buffer; export recent clips. Stream must be capturing first |
|
||||
| Camera video effects | `addVideoEffectOutput(_:)` | **iOS-only**, and only on **in-app** capture (`presentForCurrentApplication`); otherwise `SCStreamErrorNotSupported` |
|
||||
| Camera video effects | `addVideoEffectOutput(_:)` / `removeVideoEffectOutput(_:)`, both `throws`, with `SCVideoEffectOutput(cameraDevice:)` | **iOS-only**, and only on **in-app** capture (`presentForCurrentApplication`); otherwise `SCStreamErrorNotSupported`. Lifecycle arrives on `SCStreamDelegate` as three optional callbacks — `outputVideoEffectDidStart(for:)`, `outputVideoEffectDidStop(for:)`, and `outputVideoEffectDidFail(for:withError:)`; without the last one a failed effect is silent |
|
||||
|
||||
Record-to-file sketch:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
Historically iOS exposed only **one** native streaming protocol (AirPlay); supporting Chromecast etc. meant bundling each vendor's SDK and your own cast button. AVSystemRouting replaces that with **one Apple API**: the protocol is supplied by a system **route provider**, and your app drives playback through a uniform interface.
|
||||
|
||||
> **Availability is narrow and in flux.** This capability is reported to be driven by the EU Digital Markets Act, so it is **likely region-gated (EU)** and is **beta** as of the Xcode 27 betas. Treat third-party routes as *may or may not be present*: always `#available`-gate, and keep your existing AirPlay / in-app cast path as the fallback. Confirm regional + provider availability before relying on it.
|
||||
> **Availability is narrow and in flux.** This capability is reported to be driven by the EU Digital Markets Act, so it is **likely region-gated (EU)**. Treat third-party routes as *may or may not be present*: always `#available`-gate, and keep your existing AirPlay / in-app cast path as the fallback. Confirm regional + provider availability before relying on it.
|
||||
|
||||
## When to Use
|
||||
|
||||
@@ -113,7 +113,7 @@ Drop the `@MainActor` and Swift 6 rejects every write: *"main actor-isolated pro
|
||||
Use the `dataChannel` for the `.application` companion-app model. Note there are two data channels: the route exposes a non-optional `AVSystemRoute.routeDataChannel`, while the started media session exposes the optional `AVSystemRouteMediaSession.dataChannel` used above.
|
||||
|
||||
> **Use `AVPlaybackUserInterface*`, not the early-beta `AVInterface*` names.** Apple shipped an `AVInterface*`
|
||||
> family in an early 27 beta, then removed it — still gone as of Xcode 27 beta 6.
|
||||
> family in an early 27 beta, then removed it — still gone in the Xcode 27.0 RC.
|
||||
> `playbackControl` is typed `(any AVKit.AVPlaybackUserInterfaceControllable)?`. Older code or AI-suggested
|
||||
> snippets that still reference `AVInterfaceControllable` (from an outdated SDK or stale training data) fail
|
||||
> to compile — replace the whole `AVInterface*` family with its `AVPlaybackUserInterface*` equivalent.
|
||||
|
||||
@@ -1483,6 +1483,4 @@ monitor.start(queue: .main)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** 2025-12-02
|
||||
**Status** Production-ready reference from WWDC 2018 and WWDC 2025
|
||||
**Coverage** NWConnection (iOS 12+), NetworkConnection (iOS 26+), all 12 WWDC 2025 code examples
|
||||
|
||||
@@ -1206,9 +1206,3 @@ if case .failed(let error) = state {
|
||||
- Pattern 3: Weak self in Task closures (similar memory leak prevention)
|
||||
- @MainActor usage for connection state updates
|
||||
- Task cancellation when connection fails
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** 2025-12-02
|
||||
**Status** Production-ready diagnostics from WWDC 2018/2025
|
||||
**Tested** Diagnostic patterns validated against real production issues
|
||||
|
||||
@@ -603,7 +603,6 @@ For apps supporting iOS 12-18 that can't use async/await yet, see `skills/networ
|
||||
- Pattern 2c: NWListener (accepting connections, Bonjour)
|
||||
- Pattern 2d: Network Discovery (NWBrowser for service discovery)
|
||||
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
### Scenario 1: Reachability Race Condition Under App Store Deadline
|
||||
@@ -917,7 +916,6 @@ For detailed migration guides from legacy networking APIs, see `skills/networkin
|
||||
- Migration 2: NWConnection → NetworkConnection (iOS 26+)
|
||||
- Migration 3: URLSession StreamTask → NetworkConnection
|
||||
|
||||
|
||||
## Checklist
|
||||
|
||||
Before shipping networking code, verify:
|
||||
@@ -1019,9 +1017,3 @@ Before shipping networking code, verify:
|
||||
**WWDC**: 2018-715, 2025-250, 2026-265
|
||||
|
||||
**Skills**: See `skills/networking-diag.md`, `skills/network-framework-ref.md`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** 2025-12-02
|
||||
**Status** Production-ready patterns from WWDC 2018 and WWDC 2025
|
||||
**Tested** Patterns validated against Apple documentation and WWDC transcripts
|
||||
|
||||
+1
-2
@@ -404,5 +404,4 @@ Run: `/axiom-audit` energy
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-26
|
||||
**Platforms**: iOS 26+, iPadOS 26+
|
||||
**Platforms**: iOS26/iPadOS26
|
||||
|
||||
+1
-2
@@ -1098,5 +1098,4 @@ func application(
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-26
|
||||
**Platforms**: iOS 26+, iPadOS 26+
|
||||
**Platforms**: iOS26/iPadOS26
|
||||
|
||||
@@ -99,8 +99,7 @@ Typed metric structs use `Measurement`, generic `Histogram<DimensionType>` (buck
|
||||
| Case | Payload | Notes |
|
||||
|------|---------|-------|
|
||||
| `.hangTime` | `Histogram<UnitDuration>` | |
|
||||
| `.hitchTime` | ratio + totalHitchTime + totalAnimationTime | animation hitches beyond scrolling |
|
||||
| `.scrollHitchTime` | ratio + totalHitchTime + totalScrollTime | |
|
||||
| `.hitchTime` | `ratio: Measurement<HitchTimeRatio>` + `totalHitchTime` + `totalAnimationTime` (both `Measurement<UnitDuration>`) | every tracked animation — there is no scroll-specific case, see below |
|
||||
| `.timeToFirstDraw`, `.optimizedTimeToFirstDraw`, `.applicationResumeTime`, `.extendedLaunch` | `Histogram<UnitDuration>` | launch family |
|
||||
| `.foregroundTermination`, `.backgroundTermination` | per-category counts | both include watchdog; background adds taskTimeout, fileLock, highCPU, systemPressure |
|
||||
| `.cpuTime`, `.cpuInstructionsCount` | duration / count | |
|
||||
@@ -117,6 +116,10 @@ Typed metric structs use `Measurement`, generic `Histogram<DimensionType>` (buck
|
||||
| `.metalFrameRate` | framesPerSecond, frameCount, activeDrawingDuration, layerName | new capability — render performance for games |
|
||||
| `.signpostInterval` | duration histogram + optional averageMemory, cpuTime, logicalWrites, hitch ratios | per signpost name/category |
|
||||
|
||||
**Scroll hitches did not survive into the Swift API.** `MetricResult` has no `.scrollHitchTime` case and `ScrollHitchTimeMetric` does not exist — Apple's 27 release notes list both as removed (180455992) — and `MetricReport` carries no scroll-hitch entries. Use `.hitchTime`, which covers all tracked animations. Scroll-specific hitching survives only on the legacy `MXAnimationMetric.scrollHitchTimeRatio` (Part 3).
|
||||
|
||||
**Recompile for the hitch-ratio type change.** `HitchTimeMetric.ratio` and `SignpostIntervalMetric.hitchTimeRatio` are `Measurement<HitchTimeRatio>`, and `HitchTimeRatio` is a new `Dimension` subclass (milliseconds of hitching per second). Apple's 27 release notes say to rebuild against the 27 SDK to pick up the type change and avoid crashing on launch (180024784).
|
||||
|
||||
`MetricGroup` constants for filtering: `.cpu`, `.memory`, `.diskIO`, `.networkTransfer`, `.display`, `.animation`, `.applicationResponsiveness`, `.cellularCondition`, `.locationActivity`, `.gpu`, `.signpost`, `.appLaunch`, `.appRuntime`, `.appTermination`, `.diskSpaceUsage`, `.frameStatistics`.
|
||||
|
||||
### Launch Task Tracking
|
||||
@@ -428,7 +431,7 @@ func processExits(_ metrics: MXAppExitMetric) {
|
||||
@available(iOS 14.0, *)
|
||||
func processHitches(_ metrics: MXAnimationMetric) {
|
||||
// Scroll hitch rate (hitches per scroll)
|
||||
let scrollHitchRate = metrics.scrollHitchTimeRatio // Double (0.0 - 1.0)
|
||||
let scrollHitchRate = metrics.scrollHitchTimeRatio // Measurement<Unit>, dimensionless ratio
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -589,6 +589,4 @@ If you've spent >30 minutes and the leak still exists:
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-30
|
||||
**Status**: TDD-tested with pressure scenarios
|
||||
**Framework**: Objective-C, blocks (closure), ARC
|
||||
|
||||
@@ -1112,4 +1112,3 @@ See `axiom-performance (skills/metrickit-ref.md)` for comprehensive MetricKit in
|
||||
|
||||
**Targets:** iOS 14+, Swift 5.5+
|
||||
**Tools:** Instruments, Core Data
|
||||
**History:** See git log for changes
|
||||
|
||||
@@ -529,7 +529,5 @@ File protection generally works without special entitlements, but some features
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 4.0 (all protection levels)
|
||||
**Latest Updates**: iOS 26
|
||||
|
||||
@@ -424,7 +424,7 @@ Currently limited to `Int` parameters. Enables stack-allocated, fixed-size abstr
|
||||
|
||||
## Swift 6.4 Additions (OS27)
|
||||
|
||||
The 6.4 toolchain (Xcode 27) extends the ownership toolkit. These are verified against the Xcode 27.0 beta compiler:
|
||||
The 6.4 toolchain (Xcode 27) extends the ownership toolkit. These are verified against the Xcode 27.0 compiler:
|
||||
|
||||
### `borrow` / `mutate` accessors
|
||||
|
||||
@@ -450,7 +450,7 @@ struct FileHandle: ~Copyable, Equatable {
|
||||
|
||||
### Single-value & unique containers
|
||||
|
||||
The 6.4 stdlib adds lightweight ownership containers — verified usable in the Xcode 27 beta (no experimental flag). Each gates on `@available(anyAppleOS 27, *)`:
|
||||
The 6.4 stdlib adds lightweight ownership containers — verified usable in Xcode 27.0 (no experimental flag). Each gates on `@available(anyAppleOS 27, *)`:
|
||||
|
||||
| Type | Copyability | Init | Role |
|
||||
|------|-------------|------|------|
|
||||
@@ -489,7 +489,7 @@ func buildIDs() {
|
||||
func consumeIDs(_ x: consuming UniqueArray<Int>) { _ = x.count }
|
||||
```
|
||||
|
||||
`Ref`/`MutableRef` are the single-value analog of `Span`/`MutableSpan`: non-escapable, so the borrow can't outlive its source. On the concurrency side, `withTaskCancellationShield` is usable now and the single-resume `Continuation` is present but limited in this beta — see `swift-concurrency-ref`.
|
||||
`Ref`/`MutableRef` are the single-value analog of `Span`/`MutableSpan`: non-escapable, so the borrow can't outlive its source. On the concurrency side, `withTaskCancellationShield` is usable now and the single-resume `Continuation` is present but limited in 27.0 — see `swift-concurrency-ref`.
|
||||
|
||||
Paren-free optional existentials and opaque types now compile under Swift 6.4 — `var overlay: any Drawable?` and `some P?` no longer have to be written `(any Drawable)?`.
|
||||
|
||||
@@ -531,16 +531,16 @@ where S: ~Copyable & ~Escapable, S.Element == Int, S.Failure == Never {
|
||||
|
||||
Drop the `S.Failure == Never` constraint and the loop must be written `for try x in s` in a `throws` function.
|
||||
|
||||
### Still forthcoming (re-check each beta)
|
||||
### Announced for 6.4, absent from Xcode 27.0
|
||||
|
||||
Other 6.4 stdlib features are **not yet usable** as of Xcode 27 beta 6 (confirmed by compile-probe, build swiftlang-6.4.0.33.1):
|
||||
Two stdlib features announced at WWDC 2026-262 are not in Xcode 27.0 (swiftlang-6.4.0.34.1, compile-verified):
|
||||
|
||||
| Feature | State in beta |
|
||||
| Feature | State in 27.0 |
|
||||
|---------|---------------|
|
||||
| `Dictionary.mapKeyedValues` | Absent |
|
||||
| `FilePath` as a stdlib type | Still requires `import System` |
|
||||
|
||||
Treat these as forthcoming; re-probe on each new beta and fold what flips.
|
||||
Don't write code that waits for them; on a newer toolchain, compile-check before using them.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: axiom-swiftui
|
||||
description: "Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features."
|
||||
description: "Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features, iPhone Duo."
|
||||
---
|
||||
|
||||
## Cursor MCP Tool Boundary
|
||||
@@ -40,6 +40,7 @@ Delegate to the appropriate Cursor subagent when this router calls for a special
|
||||
| Custom containers / List replacement (iOS 18+) | See `skills/containers-ref.md` Part 7 |
|
||||
| Search implementation | See `skills/search-ref.md` |
|
||||
| Toolbars, ToolbarItem, sheet button placement, customization | See `skills/toolbars.md` |
|
||||
| Navigation subtitle: `navigationSubtitle`, `.title` / `.subtitle` / `.largeTitle` / `.largeSubtitle` placements (iOS 26) | See `skills/toolbars.md` (Pattern 14) |
|
||||
| Sheets, detents, popovers, fullScreenCover, presentation adaptation | See `skills/presentations.md` |
|
||||
| Multi-column Table, sortable/resizable columns (iPad/Mac; collapses to first column in compact) | See axiom-macos (skills/swiftui-differences.md) |
|
||||
| Inspector panel (`.inspector` — trailing column in regular width, sheet in compact) | See axiom-macos (skills/swiftui-differences.md) |
|
||||
@@ -47,6 +48,7 @@ Delegate to the appropriate Cursor subagent when this router calls for a special
|
||||
| Section index — the vertical A–Z index strip / alphabet scrubber on a list's trailing edge, jump-to-section | See `skills/26-ref.md` (Section Index) |
|
||||
| List section margins / insets around a `Section` | See `skills/26-ref.md` (Section Margins) |
|
||||
| Web content — `WebView` / `WebPage`, scroll modifiers, WebView-in-NavigationStack | See `skills/26-ref.md` (WebView & WebPage) |
|
||||
| iPhone Duo / foldable iPhone: poses, vertical bars, the fold, arrangements, hinge, scene accessories (SwiftUI and UIKit) | See `skills/iphone-duo.md` |
|
||||
| iOS 26 features | See `skills/26-ref.md` |
|
||||
|
||||
## Non-SwiftUI UI Routes
|
||||
@@ -102,6 +104,7 @@ digraph swiftui {
|
||||
what -> "skills/toolbars.md" [label="toolbars / sheet buttons"];
|
||||
what -> "skills/presentations.md" [label="sheets/detents/popovers"];
|
||||
what -> "skills/26-ref.md" [label="iOS 26 features"];
|
||||
what -> "skills/iphone-duo.md" [label="iPhone Duo / foldable"];
|
||||
what -> "skills/previews.md" [label="slow previews / building good previews"];
|
||||
what -> "skills/previews-ref.md" [label="preview API reference"];
|
||||
what -> "skills/debugging.md" [label="preview crashes / won't load"];
|
||||
@@ -120,7 +123,7 @@ digraph swiftui {
|
||||
- Navigation audit → Delegate to the `swiftui-nav-auditor` subagent or `/axiom-audit` swiftui-nav
|
||||
- Layout audit → Delegate to the `swiftui-layout-auditor` subagent or `/axiom-audit` swiftui-layout
|
||||
- UX flow audit → Delegate to the `ux-flow-auditor` subagent or `/axiom-audit` ux-flow
|
||||
- Liquid Glass scan → Delegate to the `liquid-glass-auditor` subagent or `/axiom-audit` liquid-glass (detects migration opportunities AND adoption-completeness gaps: variant discipline for media surfaces, glass-on-glass nesting, missing `if #available` gates, primary-action tinting, `.tabRole(.search)`; scores ADOPTED / PARTIAL / NOT ADOPTED)
|
||||
- Liquid Glass scan → Delegate to the `liquid-glass-auditor` subagent or `/axiom-audit` liquid-glass (detects migration opportunities AND adoption-completeness gaps: variant discipline for media surfaces, glass-on-glass nesting, unstyled pre-26 fallbacks, semantic toolbar placement, `Tab(role: .search)`, the `UIDesignRequiresCompatibility` opt-out; scores ADOPTED / PARTIAL / NOT ADOPTED)
|
||||
- TextKit scan → Delegate to the `textkit-auditor` subagent or `/axiom-audit` textkit (detects fallback triggers, glyph APIs that corrupt complex scripts, missing Writing Tools wiring, AND architectural gaps like missing fallback observation, SwiftUI wrappers dropping TextKit 2 properties, missing `isWritingToolsActive` guards; scores MODERN / MIXED / LEGACY)
|
||||
|
||||
## Anti-Rationalization
|
||||
@@ -139,3 +142,4 @@ digraph swiftui {
|
||||
| "`@State` is lazy in Xcode 27, I read the release notes" | Only when the property is `private`/`fileprivate`. `skills/architecture.md` has the gate, the three TN3211 breaks, and the one that compiles and is wrong at runtime. |
|
||||
| "I'll just write a wrapper view for `@State` in this preview" | `@Previewable @State` (Xcode 16+) eliminates that boilerplate. `skills/previews-ref.md` has the macro signature. |
|
||||
| "I'll just rebuild and relaunch every time" | Hot reload edits the running app in place, state preserved. `skills/hot-reload.md` has the InjectionNext + Inject setup and the verify-via-`xclog` loop. |
|
||||
| "Duo is just a bigger iPhone; my layout already resizes" | Bars move to the side, the fold divides the screen, and the outer display can't open windows. `skills/iphone-duo.md` covers what resizing alone misses. |
|
||||
|
||||
+6
-5
@@ -32,7 +32,7 @@ Comprehensive guide to new SwiftUI features in iOS 26, iPadOS 26, macOS Tahoe, w
|
||||
|
||||
## System Requirements
|
||||
|
||||
#### iOS 26+, iPadOS 26+, macOS Tahoe+, watchOS 26+, visionOS 26+
|
||||
#### OS26, not tvOS
|
||||
|
||||
---
|
||||
|
||||
@@ -98,7 +98,7 @@ Use in collapsed `NavigationSplitView` sidebar to specify which column shows sea
|
||||
|
||||
#### Other Toolbar Features
|
||||
|
||||
- `.navigationSubtitle("3 unread")` — Secondary line below title
|
||||
- `.navigationSubtitle("3 unread")` — Secondary line below title; custom subtitle placements and gotchas in `skills/toolbars.md` Pattern 14
|
||||
- `.badge(3)` on toolbar items — Notification counts
|
||||
- Monochrome icon rendering — Reduces visual noise; tint for meaning, not decoration
|
||||
- Scroll edge blur — Automatic, no code required
|
||||
@@ -173,6 +173,8 @@ CardContent()
|
||||
.background(ConcentricRectangle(corners: .concentric(minimum: .fixed(8))).fill(.background))
|
||||
```
|
||||
|
||||
On iPhone Duo, concentricity follows each display's corner shape — `ConcentricRectangle` (UIKit: `UICornerConfiguration`) fits both displays without per-device radii. See skills/iphone-duo.md (Match the new corners and support landscape).
|
||||
|
||||
| API | Notes |
|
||||
|-----|-------|
|
||||
| `ConcentricRectangle()` | All four corners `.concentric` |
|
||||
@@ -928,9 +930,9 @@ A `WebView` owns an internal scroll view, and a navigation container computes sa
|
||||
|
||||
#### Form-submission hook + navigation tweaks OS27
|
||||
|
||||
`WebPage.NavigationDeciding` gains `willSubmit(formInfo:) async` (default no-op), observing form submissions: `WebPage.FormInfo` (`@MainActor`, so implicitly `Sendable`) carries `targetFrame` / `sourceFrame` (`FrameInfo`), `submissionURL`, `httpMethod`, and `formValues: [String: String]`. `WebPage.NavigationPreferences` adds `alternateRequest: URLRequest?`, `overrideReferrer: String?`, `isGlobalPrivacyControlEnabled: Bool`, and `allowsJSHandleCreationInPageWorld: Bool`. Confirmed on iOS 27, macOS 27, and visionOS 27 (re-verified beta 6); **not watchOS/tvOS**, which are marked `unavailable` explicitly.
|
||||
`WebPage.NavigationDeciding` gains `willSubmit(formInfo:) async` (default no-op), observing form submissions: `WebPage.FormInfo` (`@MainActor`, so implicitly `Sendable`) carries `targetFrame` / `sourceFrame` (`FrameInfo`), `submissionURL`, `httpMethod`, and `formValues: [String: String]`. `WebPage.NavigationPreferences` adds `alternateRequest: URLRequest?`, `overrideReferrer: String?`, `isGlobalPrivacyControlEnabled: Bool`, and `allowsJSHandleCreationInPageWorld: Bool`. Confirmed on iOS 27, macOS 27, and visionOS 27 (re-verified on the 27.0 RC); **not watchOS/tvOS**, which are marked `unavailable` explicitly.
|
||||
|
||||
As of beta 5 — still true at beta 6 — the iOS SDK stamps **real versions for all three platforms** — `@available(macOS 27.0, iOS 27.0, visionOS 27.0, *)`. Through beta 4 it wrote `macOS 9999, visionOS 9999` (the cross-SDK "not yet stamped" sentinel), so an availability check written against an early-beta SDK may be narrower than what actually ships.
|
||||
As of beta 5 — still true in the 27.0 RC — the iOS SDK stamps **real versions for all three platforms** — `@available(macOS 27.0, iOS 27.0, visionOS 27.0, *)`. Through beta 4 it wrote `macOS 9999, visionOS 9999` (the cross-SDK "not yet stamped" sentinel), so an availability check written against an early-beta SDK may be narrower than what actually ships.
|
||||
|
||||
**tvOS**: WebView and WebPage are **not available on tvOS**. tvOS has no WKWebView at all. For web content parsing on tvOS, use JavaScriptCore. See `axiom-swift (skills/tvos.md)` for alternatives.
|
||||
|
||||
@@ -1350,4 +1352,3 @@ Apps must support resizable windows on iPad.
|
||||
---
|
||||
|
||||
**Primary source** WWDC 2025-256 "What's new in SwiftUI". Additional content from 2025-323 (Build a SwiftUI app with the new design), 2025-287 (Meet WebKit for SwiftUI), and Apple documentation.
|
||||
**Version** iOS 26+, iPadOS 26+, macOS Tahoe+, watchOS 26+, visionOS 26+
|
||||
|
||||
+2
-3
@@ -265,7 +265,7 @@ init(id: String, title: String) {
|
||||
}
|
||||
```
|
||||
|
||||
TN3211 documents the ❌ form as an error, but the diagnostic does not fire on every 27 toolchain — it does not on Xcode 27.0 (27A5252f). The compiler is not a reliable gate here; order it correctly regardless.
|
||||
TN3211 documents the ❌ form as an error, but Xcode 27.0 (27A266a) compiles it with no diagnostic. The compiler is not a reliable gate here; order it correctly regardless.
|
||||
|
||||
#### The silent one — never pair an inline initial value with an `init` assignment
|
||||
|
||||
@@ -1870,6 +1870,5 @@ struct OrderRow: View {
|
||||
|
||||
---
|
||||
|
||||
**Platforms**: iOS 26+, iPadOS 26+, macOS Tahoe+, watchOS 26+, visionOS 26+
|
||||
**Platforms**: OS26, not tvOS
|
||||
**Xcode**: 26+ (see "`@State` is a macro now" for Xcode 27 build-time changes)
|
||||
**Status**: Production-ready (v1.0)
|
||||
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
# iPhone Duo
|
||||
|
||||
iPhone Duo is Apple's first two-display iPhone: an outer display, and a larger inner display that folds along a hinge. This skill covers adapting an app to it — the device model, what changes with the SDK you build against, vertical bars, the fold, and the new layout, hinge, and scene tools. SwiftUI comes first; the UIKit equivalent sits beside it.
|
||||
|
||||
## SDK Status — Read First
|
||||
|
||||
Apple announced the Duo-specific APIs in six tech talks (111461–111466, September 2026). They're announced for the **iOS 27.1 SDK**, which was unreleased — not even in beta — when this skill was written (2026-09-11). Everything marked iOS 27.0 or earlier compiles today.
|
||||
|
||||
- **Check the installed SDK first** (`xcrun --sdk iphoneos --show-sdk-version`). Below 27.1: never write an API from the table in code as if it compiles — describe it, name the talk, and give today's alternative. On 27.1 or later, betas included: grep the SDK's `.swiftinterface` and headers for the name; if it's there, the SDK's spelling and signature win over this table; if it's missing, say it was renamed or dropped.
|
||||
- **Never call iPhone Duo or these APIs fictional, hallucinated, or unannounced.** They come from Apple's own tech talks.
|
||||
- **Never invent parameters, types, or cases** the table doesn't give.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when:
|
||||
- Preparing an app for iPhone Duo, a foldable iPhone, or a two-display iPhone
|
||||
- Layout breaks in some poses — closed, open, rotated, or partially folded
|
||||
- Toolbar, tab bar, or navigation bar items should move to the side of the screen
|
||||
- Interactive UI lands in the fold or under the inner camera
|
||||
- Choosing between reserved regions, arrangements, and the hinge
|
||||
- Showing content on another display, or opening multiple windows on iPhone
|
||||
- Code or a question names an API from the 27.1 table (`onHingeChange`, `ArrangementView`, `reservedRegions`, `axisBehavior`, …)
|
||||
|
||||
#### Related Skills
|
||||
- axiom-uikit (skills/uikit-modernization.md) — the resizing baseline Duo builds on: scene lifecycle, geometry, size classes
|
||||
- skills/layout.md — adaptive layout; the size-class truth tables include Duo
|
||||
- skills/toolbars.md — SwiftUI placements, overflow, and visibility priority
|
||||
- axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md) — Duo front cameras and camera direction
|
||||
|
||||
## Example Prompts
|
||||
|
||||
#### 1. "How do I prepare my app for iPhone Duo?"
|
||||
→ Readiness Today: resizing baseline, per-side safe areas, standard containers; build with the 27.1 SDK once it ships.
|
||||
|
||||
#### 2. "How do I detect that my app is running on iPhone Duo?"
|
||||
→ Don't. It's still an iPhone app — use size classes and scene geometry.
|
||||
|
||||
#### 3. "My toolbar buttons should move to the side on iPhone Duo."
|
||||
→ Vertical Bars: system-managed bars only, titles on every item, overflow priorities.
|
||||
|
||||
#### 4. "A button sits in the fold when the phone is partly closed."
|
||||
→ The Fold and the Camera: displacement rules.
|
||||
|
||||
#### 5. "Can I use the hinge angle in my app?"
|
||||
→ Hinge: effects and interactions, never layout.
|
||||
|
||||
#### 6. "Add .axisBehavior(.horizontalOnly) to my Select button"
|
||||
→ Announced for 27.1 (table); 27.0 builds never get vertical bars — ship titles, images, and priorities now.
|
||||
|
||||
## Red Flags — Anti-Patterns to Prevent
|
||||
|
||||
| Thought | Reality |
|
||||
|---|---|
|
||||
| "I'll check the model identifier and give Duo its own layout" | It's still an iPhone app — compact width outside, regular width inside. Model and idiom checks break in Split View, in iPhone Mirroring, and on the next device. Use size classes and scene geometry. |
|
||||
| "I'll branch on interface orientation" | The inner display ignores your supported orientations. Decide layout with size classes. |
|
||||
| "`UIScreen.main` gives me the screen" | Deprecated since iOS 26, and ambiguous with two displays. Use `window?.windowScene?.screen` or `traitCollection.displayScale`. |
|
||||
| "Safe-area insets are symmetric" | A vertical bar sits on one side, so left ≠ right — and in Split View it switches sides. Inset each side independently. |
|
||||
| "I'll build my own bottom bar" | A custom `UIToolbar`/`UINavigationBar`/`UITabBar` or a hand-built SwiftUI row never moves to the side, and a hand-built row gets none of the system's fold avoidance. Use system-managed bars. |
|
||||
| "I'll hide the controls when it's folded" | Displace, never hide: move, resize, or reorganize so every function stays reachable in every pose. |
|
||||
| "I'll read the hinge angle to size my panes" | The hinge drives effects and interactions. Layout uses arrangements and reserved regions. |
|
||||
| "Each pose gets its own layout" | Design for the two horizontal size classes — compact outside, regular inside. An optional tabletop layout must keep every control and the same hierarchy. |
|
||||
| "The New Window button can always show" | The outer display can't create windows. Gate the affordance. |
|
||||
|
||||
## The Device
|
||||
|
||||
| Display | Horizontal | Vertical |
|
||||
|---|---|---|
|
||||
| Outer, portrait | `.compact` | `.regular` |
|
||||
| Outer, landscape | `.compact` | `.compact` |
|
||||
| Inner | `.regular` | `.regular` |
|
||||
|
||||
- **Poses** Closed; open in portrait or landscape; partially folded like a book; seated like a laptop (tabletop) with the inner display facing you; standing on its edges.
|
||||
- **Still an iPhone app** Adapt to size classes and scene bounds, never to the device.
|
||||
- **Controls on the side** Built against the 27.1 SDK, in every pose except inner-display portrait, bars lay out vertically along the side, sharing that edge with the status bar, the Dynamic Island, and Live Activities. When space runs out, items collapse into the overflow menu.
|
||||
- **Multitasking** A 50/50 split view places two apps side by side, each with its controls on its outer edge. Picture in Picture can pin to the top; the app below resizes vertically.
|
||||
- **Offset, don't center** Most content offsets away from the side controls — align to horizontal safe-area insets and it happens for you. Center on the full display only for non-scrolling, highly visual UI whose interactive elements the controls can't cover. A full-width background under inset scrolling content also works.
|
||||
- **Inner display** Don't stretch the iPhone layout. Use a split view, a two-column rearrangement when width allows, or a tab sidebar for information-dense apps. Keep the hierarchy identical inside and out — people open and close the device mid-task.
|
||||
|
||||
## Behavior by the SDK You Build Against
|
||||
|
||||
Link-time behavior — what the device does with your binary:
|
||||
|
||||
| Built against | On iPhone Duo |
|
||||
|---|---|
|
||||
| Pre-27 SDK | Closed: runs in the space beside the status bar and camera. Open: a familiar size and aspect ratio |
|
||||
| iOS 27.0 SDK | Resizes like any 27 iPhone app; extends left of the status bar on the inner display |
|
||||
| iOS 27.1 SDK | Edge to edge; standard navigation, toolbar, and tab bars lay out vertically |
|
||||
|
||||
`UIRequiresFullScreen` is still honored, but the app still resizes when the device opens or closes. Supported orientations govern the outer display as on any iPhone; the inner display doesn't honor them — the app scales there instead, including in Split View.
|
||||
|
||||
## Which Tool When
|
||||
|
||||
| Need | Tool | Availability |
|
||||
|---|---|---|
|
||||
| Navigation, tabs, sheets, alerts, and menus that adapt to every pose | Standard containers (`NavigationSplitView`, `TabView`, `UISplitViewController`, …) | Today |
|
||||
| Custom UI that must avoid the fold or the inner camera | Reserved regions | 27.1, announced |
|
||||
| Two views that split side by side or overlay | Arrangements | 27.1, announced |
|
||||
| An effect or interaction driven by the fold angle | Hinge | 27.1, announced |
|
||||
| Extra content on another display | Scene accessories | Today for external displays; Duo camera variant 27.1, announced |
|
||||
| A second window of your app | Multiple scenes, inner display only | Today |
|
||||
|
||||
## Readiness Today
|
||||
|
||||
The resizing baseline is the same as for every 27 iPhone app — scene lifecycle, no `UIScreen.main`, size classes over orientation and idiom: axiom-uikit (skills/uikit-modernization.md). Adaptive patterns: skills/layout.md. Duo adds five things.
|
||||
|
||||
#### Handle each safe-area side independently
|
||||
|
||||
```swift
|
||||
// Wrong on Duo: a vertical bar on one side makes left != right
|
||||
let assumedWidth = view.bounds.width - view.safeAreaInsets.left * 2
|
||||
// Right: inset each side on its own
|
||||
let contentWidth = view.bounds.inset(by: view.safeAreaInsets).width
|
||||
```
|
||||
|
||||
Layout margins are asymmetric too. SwiftUI places content inside the safe area by default; let backgrounds extend with `.ignoresSafeArea()`, and in UIKit size backgrounds to `view.bounds`. With Auto Layout, constrain to `safeAreaLayoutGuide` / `layoutMarginsGuide` — each side stays independent (axiom-uikit (skills/adaptive-layout.md)). Under right-to-left languages the bar stays on the hardware side, so never assume the trailing inset is the larger one.
|
||||
|
||||
#### Offer a tab sidebar on the inner display `iOS27`
|
||||
|
||||
```swift
|
||||
TabView {
|
||||
Tab("Summary", systemImage: "heart") { SummaryView() }
|
||||
Tab("Browse", systemImage: "square.grid.2x2") { BrowseView() }
|
||||
}
|
||||
.tabViewStyle(.sidebarAdaptable) // required: the placement applies only to this style
|
||||
.defaultTabBarPlacement(.sidebar)
|
||||
```
|
||||
|
||||
UIKit: `tabBarController.sidebar.preferredPlacement = .sidebar`. The SwiftUI modifier has no effect on iPadOS, where the bar adapts on its own — use `defaultAdaptableTabBarPlacement(_:)` there. A sidebar suits information-dense apps; most apps keep the tab bar.
|
||||
|
||||
#### Gate new-window affordances
|
||||
|
||||
iPhone Duo is the first iPhone with multiple windows of one app, and only its inner display can create them. Both paths need `UIApplicationSupportsMultipleScenes` set to `YES` in the scene manifest (axiom-uikit (skills/uikit-modernization.md)), and `openWindow(id:value:)` needs a matching `WindowGroup(id:for:)` (axiom-design (skills/app-composition.md)). Apple's own doc comment for `supportsMultipleWindows` limits `true` to macOS (any SwiftUI-lifecycle app) and iPadOS (a SwiftUI-lifecycle app with that manifest key set) — every other platform and configuration reports `false`. On today's SDK that means the value is `false` on iPhone; the talks don't say how Duo's inner display changes it.
|
||||
|
||||
```swift
|
||||
struct ItemRow: View {
|
||||
let item: Item
|
||||
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
|
||||
var body: some View {
|
||||
Text(item.title)
|
||||
.contextMenu {
|
||||
if supportsMultipleWindows {
|
||||
Button("Open in New Window") { openWindow(id: "detail", value: item.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// UIKit menu action (iOS 15): where new windows aren't available the alternate runs;
|
||||
// with no alternate, the talks say the item hides (111464)
|
||||
let openHere = UIAction(title: "Open") { _ in showDetailInCurrentWindow() }
|
||||
let newWindow = UIWindowScene.ActivationAction(alternate: openHere) { _ in
|
||||
UIWindowScene.ActivationConfiguration(userActivity: detailActivity)
|
||||
}
|
||||
|
||||
// Direct request: fails on the outer display, so handle the error (iOS 17)
|
||||
let request = UISceneSessionActivationRequest(role: .windowApplication, userActivity: detailActivity)
|
||||
UIApplication.shared.activateSceneSession(for: request) { error in
|
||||
logger.error("Window request failed: \(error.localizedDescription, privacy: .public)")
|
||||
showDetailInCurrentWindow()
|
||||
}
|
||||
```
|
||||
|
||||
The Swift name is `UIWindowScene.ActivationAction`; the ObjC name `UIWindowSceneActivationAction` doesn't compile in Swift. The talks don't say whether `supportsMultipleWindows` updates live as the device closes — confirm on the Duo simulator once Xcode 27.1 ships.
|
||||
|
||||
#### Match the new corners and support landscape
|
||||
|
||||
`ConcentricRectangle` / `UICornerConfiguration` (iOS 26) are updated for Duo's display corners — skills/26-ref.md (Corner Concentricity). Support landscape on the outer display; people may set the phone down like a tent.
|
||||
|
||||
#### Test both halves of Split View
|
||||
|
||||
Drag the app to the left half, then the right. The vertical bar follows the app's outer edge, so the larger safe-area inset switches sides.
|
||||
|
||||
## Vertical Bars
|
||||
|
||||
Built against the 27.1 SDK, navigation, toolbar, and tab bar items share one vertical stack along the side — picture the horizontal bars rotated 90°. Only **system-managed bars** take part: `NavigationStack`, `NavigationSplitView`, or `TabView` with `.toolbar`, or `UINavigationController` and `UITabBarController` with items set on the view controller. The content of a custom `UIToolbar`, `UINavigationBar`, or `UITabBar` never joins the vertical bar; it stays where you put it.
|
||||
|
||||
#### Which bars go vertical
|
||||
|
||||
- Only a split view's detail column; other columns keep horizontal bars. Inspectors get no vertical bar.
|
||||
- Sheets: on the outer display a sheet's toolbar goes vertical; on the inner display sheets center with horizontal bars. A sheet placed on the right gets a vertical bar; one on the left doesn't. The placement APIs — `.presentationPlacement(.trailing)` and `sheetPresentationController?.preferredPlacement = .trailing`, both iOS 27; the UIKit one is ignored when `sourceView` is set — take leading/trailing, but the rule is physical right/left.
|
||||
- The bar stays on the hardware side under right-to-left languages; content adapts around it.
|
||||
- Keyboard accessory bars stay on the keyboard.
|
||||
- The inner display in portrait keeps horizontal bars.
|
||||
|
||||
#### Order items top to bottom
|
||||
|
||||
1. Back (automatic in a navigation container) or a custom close. SwiftUI: `.cancellationAction`. UIKit: a leading item, with `leftItemsSupplementBackButton` left `false` (the default).
|
||||
2. The prominent action. SwiftUI: `.topBarPinnedTrailing` `iOS27`. UIKit: `pinnedTrailingGroup`.
|
||||
3. Everything else, in its existing groups. A spacer separates top and bottom placements.
|
||||
|
||||
Top-bar items go to the top of the stack, bottom-bar items to the bottom, and the tab bar stays bottom-aligned.
|
||||
|
||||
Keep placement consistent across poses so people don't relearn where actions live. SwiftUI code for these placements and for overflow: skills/toolbars.md (Pattern 2, Pattern 11). UIKit:
|
||||
|
||||
```swift
|
||||
navigationItem.leftItemsSupplementBackButton = false // the custom close replaces back
|
||||
navigationItem.leadingItemGroups = [UIBarButtonItemGroup(barButtonItems: [closeItem], representativeItem: nil)]
|
||||
navigationItem.pinnedTrailingGroup = UIBarButtonItemGroup(barButtonItems: [doneItem], representativeItem: nil)
|
||||
navigationItem.additionalOverflowItems = UIDeferredMenuElement.uncached { completion in
|
||||
completion([UIAction(title: "Scan", image: UIImage(systemName: "doc.viewfinder")) { _ in scan() }])
|
||||
}
|
||||
shareItem.visibilityPriority = .high // iOS27: collapses after standard-priority items
|
||||
inboxItem.badge = .count(7) // iOS 26: a symbol-only item that still shows the count
|
||||
```
|
||||
|
||||
`UIBarButtonItem.Badge` is itself main-actor isolated — even `.count(n)` can't be built in a nonisolated model.
|
||||
|
||||
#### Make items vertical-ready
|
||||
|
||||
- Give every item a title and an image — a SwiftUI `Label`, or a `UIBarButtonItem` with both. Bars show the icon; the overflow menu shows title and icon.
|
||||
- Items with an icon go vertical; text-only items stay horizontal. An item that switches between a symbol and text (a custom Select/Done) belongs on the horizontal axis — the system edit button already stays there.
|
||||
- Replace inline counts with a badge. Text that carries real information, like a cart total, stays in a horizontal bar.
|
||||
- Custom views, complex views, and wide controls like segmented controls stay horizontal unless opted in (table).
|
||||
- Vertical bars have a fixed width and flexible height; once a custom view opts in, it must fit that width or adapt its layout. Flexible spacers collapse to zero vertically; fixed spacers keep their minimum. Don't add extra spacing.
|
||||
- Vertical bars have no scroll-edge effect but gain a background under Reduce Transparency — keep custom content legible either way.
|
||||
|
||||
#### Plan for overflow
|
||||
|
||||
The outer display in landscape overflows most. Decide per view whether the toolbar or the tab bar compresses first — navigation-focused views keep their tabs, task-focused views keep their actions. By default the toolbar compresses first and the tabs stay; a task-focused view opts into keeping its actions (announced API, table). Merge your own overflow menu into the system one, keep the ellipsis for overflow only, and rank items with `visibilityPriority`: frequent actions and badged status items should collapse last. By default items overflow from the bottom up. A non-nil `additionalOverflowItems` always shows the overflow button. The keyboard and Picture in Picture in open portrait also shrink the bar.
|
||||
|
||||
#### When to turn vertical bars off
|
||||
|
||||
A single-page, bottom-heavy layout like a calculator, or a sheet whose only item is Close, may work better with horizontal bars.
|
||||
|
||||
Axis overrides, the vertical-edge query, the compression preference, and the switch that turns vertical bars off are announced for 27.1 — see the table below.
|
||||
|
||||
## The Fold and the Camera
|
||||
|
||||
When iPhone Duo is partially folded, the display curves through the center and splits into regions. Two kinds of **reserved region** shape the usable space:
|
||||
|
||||
- **Division** — the fold. Active only while partially folded; zero width when flat.
|
||||
- **Occlusion** — the inner FaceTime camera. Active only while that camera runs.
|
||||
|
||||
The outer display's camera is always present, and system bars already lay out around it.
|
||||
|
||||
#### Displacement rules
|
||||
|
||||
- Move, resize, or reorganize — never hide. Every function stays reachable in every pose.
|
||||
- Move elements that work together as a unit; move independent elements alone.
|
||||
- Avoid long moves; distance weakens the link between an element and its source.
|
||||
- Scrolling content — articles, feeds, lists — never displaces; it already adapts by scrolling.
|
||||
- Let purpose choose the destination. Book pose: alerts move to the trailing side, where they'll be when the device closes. Tabletop: content meant to be seen from a distance goes to the top region; tappable controls go to the bottom, a stable surface.
|
||||
- Stay contextual: search stays over the view it searches.
|
||||
|
||||
System components already avoid the fold: sheets, alerts, action sheets, menus, popovers, and toolbar buttons, and split views rebalance to an even 50/50. Use them wherever you can. Inactive regions still inform high-level choices — prefer an even number of grid columns when a fold can appear.
|
||||
|
||||
Querying regions is announced for 27.1 — see the table below.
|
||||
|
||||
## Arrangements
|
||||
|
||||
An arrangement places a primary and a secondary view by rules — size classes, aspect ratio, and active fold regions. It sits between navigation containers and content containers.
|
||||
|
||||
- **Split** — main and detail content where neither view may be obscured, like a player and its transcript. The default style; it splits along the longer axis unless restricted. If the split can't use the view's long axis (e.g. `.axes(.horizontal)` in a tall view), it shows a single view (the primary, in the talk's example) — keep the secondary reachable another way.
|
||||
- **Overlay** — a clear foreground and background, like controls over readable content. It layers one view over the other, and goes side by side when folded.
|
||||
- Follow existing patterns: an HStack or VStack split becomes a split arrangement; a ZStack overlay becomes an overlay arrangement.
|
||||
- Never nest a navigation container inside an arrangement, and never put an arrangement inside a `List` or `ScrollView`.
|
||||
- Put it inside the navigation container — the talks nest it in a `NavigationStack` and make the UIKit controller the navigation root. Use it for split-like layout without a split view's expand/collapse.
|
||||
|
||||
The API is announced for 27.1 — see the table below.
|
||||
|
||||
## Hinge
|
||||
|
||||
The hinge reports a status — closed, partially open, fully open — and a continuous angle. Use it for effects and interactions, like a pitch bend or a zoom that follows the fold, never for layout. A missing hinge means the device has none; reset hinge-driven state whenever the device isn't partially open. The API is announced for 27.1 — see the table below.
|
||||
|
||||
## Scenes and Accessories
|
||||
|
||||
A **scene accessory** is supplementary content the system presents for you when a capability becomes available. The system decides when and where it appears, and your app must stay fully functional without it.
|
||||
|
||||
#### External-display accessory `iOS27`
|
||||
|
||||
Today's accessory targets an external display, connected or over AirPlay:
|
||||
|
||||
```swift
|
||||
struct PresenterView: View {
|
||||
let deck: Deck
|
||||
@State private var showsAudienceView = true
|
||||
@State private var accessoryAvailable = false
|
||||
|
||||
var body: some View {
|
||||
SlideEditor(deck: deck)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Toggle("Audience View", systemImage: "rectangle.on.rectangle", isOn: $showsAudienceView)
|
||||
.disabled(!accessoryAvailable)
|
||||
}
|
||||
}
|
||||
.sceneAccessory {
|
||||
ExternalNonInteractiveAccessory(isEnabled: $showsAudienceView) {
|
||||
AudienceSlide(deck: deck)
|
||||
}
|
||||
.onAvailabilityChange { accessoryAvailable = $0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register the accessory on the view whose visibility should gate it.
|
||||
|
||||
#### The Duo camera accessory
|
||||
|
||||
On iPhone Duo, a camera variant shows UI on the outer display — a teleprompter, or something to show the person being photographed — while your camera UI runs on the inner display. It's available only while the app is full screen on the inner display with an active camera session, and it's announced for 27.1 (table below). Camera direction and the new front cameras: axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md).
|
||||
|
||||
## Announced for the iOS 27.1 SDK
|
||||
|
||||
From Apple's tech talks 111461–111466. **Absent from the 27.0 SDK; announced for 27.1, and names can change before it ships. Don't write them in code as if they compile. Don't call them fictional. Don't fill in parameters, types, or cases this table doesn't give.** Spellings follow the talks' code where the narration differs. Re-check each Xcode release.
|
||||
|
||||
| Key | SwiftUI | UIKit | Behavior | Talk |
|
||||
|---|---|---|---|---|
|
||||
| `bars.axis` | `.axisBehavior(.verticalPreferred)` / `.horizontalOnly` on a `ToolbarItem` | `UIBarButtonItem.axisBehavior` | Overrides the inferred axis; custom views stay horizontal unless `.verticalPreferred` | 111462 8:08 |
|
||||
| `bars.edge` | `@Environment(\.toolbarVerticalEdge)` | `traitCollection.verticalBarEdge` | Which edge holds the vertical bar; nil or unspecified when items can't go vertical | 111462 10:36 |
|
||||
| `bars.compression` | `.toolbarVerticalCompressionBehavior(.prefersToolbarItems)` (narration: "toolbarCompressionBehavior") | `navigationItem.verticalBarCompressionBehavior = .prefersBarItems` | Chooses whether toolbar items or the tab bar compress first. Default: toolbar compresses first. `.prefersToolbarItems` / `.prefersBarItems`: the tab bar compresses first | 111462 12:23 |
|
||||
| `bars.disable` | `.toolbarVerticalBehavior(.disabled)` | `override var preferredVerticalBarBehavior: UIVerticalBarBehavior` returning `.disabled` | Keeps horizontal bars | 111462 14:47 |
|
||||
| `regions.query` | `GeometryProxy.reservedRegions(kind: .division` or `.occlusion`, `options: .includeInactive)` | `UIView.reservedRegions(kind:)` | Returns regions with a `frame` — elements are `ReservedRegion` / `UIViewReservedRegion` (111461 7:37); active ones only unless `.includeInactive`; for custom bars and edge-to-edge UI (111461 8:27) | 111463 6:46 |
|
||||
| `arrangement.view` | `ArrangementView { primary } secondary: { … }` | `UIArrangementViewController` with `setViewController(_:for: .primary` / `.secondary)` | Two-view layout container | 111463 11:23 |
|
||||
| `arrangement.style` | `.arrangementViewStyle(.split)`, `.split.axes(.horizontal)`, `.overlay` | `updateArrangement(_:)` with a `UISplitArrangement`, e.g. `.split.axes(.horizontal)` | Picks split or overlay; restricts split axes | 111463 12:00 |
|
||||
| `arrangement.zindex` | `@Environment(\.overlayArrangementZIndex)` | `state(for: .primary)?.zIndex` | Overlay stacking order; changes as the device folds | 111463 14:07 |
|
||||
| `hinge` | `.onHingeChange { old, new in }` — `new.hinge?.status == .partiallyOpen`, `.angle` (an `Angle`) | `UIHingeInteraction` | Hinge status and live angle; nil hinge on devices without one | 111464 1:44 |
|
||||
| `accessory.camera` | `CameraCaptureAccessory { … }` or `CameraCaptureAccessory(isEnabled:) { … }`, with `.onAvailabilityChange`, inside `.sceneAccessory` | — | Outer-display UI during an inner-display camera session | 111464 5:43 |
|
||||
|
||||
- Xcode 27.1 Device Hub: an iPhone Duo simulator with open, close, rotate, and fold controls (111461 0:56).
|
||||
- Xcode's app-modernization agent skill, renamed "App Resizability", now covers SwiftUI and iPhone Duo (111461 9:15).
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
#### "Ship Duo support by Friday — just check for the Duo model"
|
||||
A model check covers one device and breaks in Split View and iPhone Mirroring. Size classes and per-side safe areas take the same time and cover every pose. Push back: "Size classes handle Duo and every future device; a model check handles one."
|
||||
|
||||
#### "That API doesn't exist — drop the Duo section"
|
||||
The APIs come from Apple's September 2026 tech talks and are announced for the iOS 27.1 SDK. Keep the guidance, write no code against the announced APIs, and ship today's alternatives.
|
||||
|
||||
#### "Just hide the controls when it's folded"
|
||||
Hiding ties functionality to a pose. Move the controls to the region that suits their purpose; system components already do this.
|
||||
|
||||
## Checklist
|
||||
|
||||
- ☑ No model, idiom, or orientation checks drive layout
|
||||
- ☑ No `UIScreen.main`; geometry comes from the scene or the view
|
||||
- ☑ Safe-area and margin math handles each side independently
|
||||
- ☑ Corner configuration matches Duo's display corners; the outer display supports landscape
|
||||
- ☑ Bars are system-managed; every item has a title and an image
|
||||
- ☑ Items run back/close → prominent → the rest; overflow priorities are set
|
||||
- ☑ New-window affordances are gated; scene-request errors are handled
|
||||
- ☑ Interactive UI stays out of the fold through system components or displacement — never by hiding
|
||||
- ☑ Tested closed, open in both orientations, partially folded, and in both halves of Split View
|
||||
- ☑ No 27.1 API appears in code as if it compiles
|
||||
|
||||
## Resources
|
||||
|
||||
**Tech Talks**: 111461, 111462, 111463, 111464, 111465, 111466
|
||||
|
||||
**Docs**: /swiftui/view/defaulttabbarplacement(_:), /swiftui/view/sceneaccessory(content:), /swiftui/externalnoninteractiveaccessory, /swiftui/environmentvalues/supportsmultiplewindows, /uikit/uiwindowscene/activationaction, /uikit/uiapplication/activatescenesession(for:errorhandler:), /uikit/uinavigationitem/pinnedtrailinggroup
|
||||
|
||||
**Skills**: axiom-uikit (skills/uikit-modernization.md), skills/layout.md, skills/toolbars.md, skills/presentations.md, axiom-media (skills/camera-capture.md, skills/camera-capture-ref.md)
|
||||
+10
@@ -287,6 +287,16 @@ UIKit's `readableContentGuide` does all of this automatically, including the Dyn
|
||||
|
||||
**Key insight:** Size class only goes `.compact` on iPad at ~33% width or Slide Over. For finer control, use geometry.
|
||||
|
||||
## Size Class Truth Table (iPhone Duo)
|
||||
|
||||
| Display | Horizontal | Vertical |
|
||||
|---|---|---|
|
||||
| Outer, portrait | `.compact` | `.regular` |
|
||||
| Outer, landscape | `.compact` | `.compact` |
|
||||
| Inner | `.regular` | `.regular` |
|
||||
|
||||
Opening the device moves the app to the inner display mid-session: horizontal becomes `.regular`, and vertical does too if the outer display was in landscape — adapt, and keep state. Full guidance: skills/iphone-duo.md.
|
||||
|
||||
---
|
||||
|
||||
## iOS 26 Free-Form Windows
|
||||
|
||||
@@ -1356,9 +1356,3 @@ var path: NavigationPath { NavigationPath() } // ❌ Reset every time
|
||||
- Pattern 3: @MainActor isolation patterns
|
||||
- Async/await with UI updates
|
||||
- Task cancellation handling
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** 2025-12-05
|
||||
**Status** Production-ready diagnostics
|
||||
**Tested** Diagnostic patterns validated against common navigation issues
|
||||
|
||||
@@ -1033,5 +1033,4 @@ NavigationPath(codableRepresentation) // For decoding
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** Based on WWDC 2022-10054, WWDC 2024-10147, WWDC 2025-256, WWDC 2025-323 (Build a SwiftUI app with the new design)
|
||||
**Platforms** iOS 16+, iPadOS 16+, macOS 13+, watchOS 9+, tvOS 16+
|
||||
|
||||
-1
@@ -823,5 +823,4 @@ checks and fallback UI for older devices."
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** Based on WWDC 2022-2025 navigation sessions
|
||||
**Platforms** iOS 18+, iPadOS 18+, macOS 15+, watchOS 11+, tvOS 18+
|
||||
|
||||
@@ -70,6 +70,7 @@ Button("Filters") { showingFilters = true }
|
||||
- Presentation modifiers (`presentationDetents`, `presentationCompactAdaptation`, ...) go on the presented **content**, not on the presenting view.
|
||||
- System presentation containers ship with system-maintained dismissal affordances, and standard controls receive translated indirect input (trackpad, scroll devices, iPhone Mirroring) correctly. A custom ZStack overlay posing as a presentation only gets whatever gestures you wire, and custom pan/drag handling needs explicit indirect-input support — one more reason to use the system containers (see axiom-uikit (skills/uikit-modernization.md)).
|
||||
- Keyboard inside a sheet: `scrollDismissesKeyboard(.interactively)` on the sheet's scroll view controls how scrolling dismisses it.
|
||||
- **iPhone Duo**: sheets adapt per display — a sheet's toolbar goes vertical on the outer display, sheets center with horizontal bars on the inner display, and a partially folded device slides sheets clear of the fold. Rules and exceptions: skills/iphone-duo.md (Vertical Bars; The Fold and the Camera).
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -726,5 +726,4 @@ For bottom-aligned search, `.searchToolbarBehavior(.minimize)`, `Tab(role: .sear
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** Based on WWDC 2021-10176 "Searchable modifier", sosumi.ai API reference
|
||||
**Platforms** iOS 15+, iPadOS 15+, macOS 12+, watchOS 8+, tvOS 15+
|
||||
|
||||
@@ -1134,6 +1134,5 @@ Problem likely elsewhere:
|
||||
|
||||
---
|
||||
|
||||
**Xcode:** 26+
|
||||
**Platforms:** iOS 26+, iPadOS 26+, macOS Tahoe+, visionOS 3+
|
||||
**History:** See git log for changes
|
||||
**Xcode**: 26+
|
||||
**Platforms**: OS26, not watchOS/tvOS
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user