mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Merge pull request #250 from OkminLee/feat/skills/ios26-three-skills
LGTM — 3 iOS 26 skills. Pure documentation.
This commit is contained in:
@@ -143,7 +143,7 @@ For manual install instructions see the README in the `rules/` folder.
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **That's it!** You now have access to 13 agents, 44 skills, and 32 commands.
|
||||
✨ **That's it!** You now have access to 13 agents, 48 skills, and 32 commands.
|
||||
|
||||
---
|
||||
|
||||
@@ -252,6 +252,10 @@ everything-claude-code/
|
||||
| |-- swift-actor-persistence/ # Thread-safe Swift data persistence with actors (NEW)
|
||||
| |-- swift-protocol-di-testing/ # Protocol-based DI for testable Swift code (NEW)
|
||||
| |-- search-first/ # Research-before-coding workflow (NEW)
|
||||
| |-- skill-stocktake/ # Audit skills and commands for quality (NEW)
|
||||
| |-- liquid-glass-design/ # iOS 26 Liquid Glass design system (NEW)
|
||||
| |-- foundation-models-on-device/ # Apple on-device LLM with FoundationModels (NEW)
|
||||
| |-- swift-concurrency-6-2/ # Swift 6.2 Approachable Concurrency (NEW)
|
||||
|
|
||||
|-- commands/ # Slash commands for quick execution
|
||||
| |-- tdd.md # /tdd - Test-driven development
|
||||
@@ -811,7 +815,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
||||
|---------|-------------|----------|--------|
|
||||
| Agents | ✅ 13 agents | ✅ 12 agents | **Claude Code leads** |
|
||||
| Commands | ✅ 32 commands | ✅ 24 commands | **Claude Code leads** |
|
||||
| Skills | ✅ 44 skills | ✅ 16 skills | **Claude Code leads** |
|
||||
| Skills | ✅ 48 skills | ✅ 16 skills | **Claude Code leads** |
|
||||
| Hooks | ✅ 3 phases | ✅ 20+ events | **OpenCode has more!** |
|
||||
| Rules | ✅ 8 rules | ✅ 8 rules | **Full parity** |
|
||||
| MCP Servers | ✅ Full | ✅ Full | **Full parity** |
|
||||
|
||||
243
skills/foundation-models-on-device/SKILL.md
Normal file
243
skills/foundation-models-on-device/SKILL.md
Normal file
@@ -0,0 +1,243 @@
|
||||
---
|
||||
name: foundation-models-on-device
|
||||
description: Apple FoundationModels framework for on-device LLM — text generation, guided generation with @Generable, tool calling, and snapshot streaming in iOS 26+.
|
||||
---
|
||||
|
||||
# FoundationModels: On-Device LLM (iOS 26)
|
||||
|
||||
Patterns for integrating Apple's on-device language model into apps using the FoundationModels framework. Covers text generation, structured output with `@Generable`, custom tool calling, and snapshot streaming — all running on-device for privacy and offline support.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building AI-powered features using Apple Intelligence on-device
|
||||
- Generating or summarizing text without cloud dependency
|
||||
- Extracting structured data from natural language input
|
||||
- Implementing custom tool calling for domain-specific AI actions
|
||||
- Streaming structured responses for real-time UI updates
|
||||
- Need privacy-preserving AI (no data leaves the device)
|
||||
|
||||
## Core Pattern — Availability Check
|
||||
|
||||
Always check model availability before creating a session:
|
||||
|
||||
```swift
|
||||
struct GenerativeView: View {
|
||||
private var model = SystemLanguageModel.default
|
||||
|
||||
var body: some View {
|
||||
switch model.availability {
|
||||
case .available:
|
||||
ContentView()
|
||||
case .unavailable(.deviceNotEligible):
|
||||
Text("Device not eligible for Apple Intelligence")
|
||||
case .unavailable(.appleIntelligenceNotEnabled):
|
||||
Text("Please enable Apple Intelligence in Settings")
|
||||
case .unavailable(.modelNotReady):
|
||||
Text("Model is downloading or not ready")
|
||||
case .unavailable(let other):
|
||||
Text("Model unavailable: \(other)")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core Pattern — Basic Session
|
||||
|
||||
```swift
|
||||
// Single-turn: create a new session each time
|
||||
let session = LanguageModelSession()
|
||||
let response = try await session.respond(to: "What's a good month to visit Paris?")
|
||||
print(response.content)
|
||||
|
||||
// Multi-turn: reuse session for conversation context
|
||||
let session = LanguageModelSession(instructions: """
|
||||
You are a cooking assistant.
|
||||
Provide recipe suggestions based on ingredients.
|
||||
Keep suggestions brief and practical.
|
||||
""")
|
||||
|
||||
let first = try await session.respond(to: "I have chicken and rice")
|
||||
let followUp = try await session.respond(to: "What about a vegetarian option?")
|
||||
```
|
||||
|
||||
Key points for instructions:
|
||||
- Define the model's role ("You are a mentor")
|
||||
- Specify what to do ("Help extract calendar events")
|
||||
- Set style preferences ("Respond as briefly as possible")
|
||||
- Add safety measures ("Respond with 'I can't help with that' for dangerous requests")
|
||||
|
||||
## Core Pattern — Guided Generation with @Generable
|
||||
|
||||
Generate structured Swift types instead of raw strings:
|
||||
|
||||
### 1. Define a Generable Type
|
||||
|
||||
```swift
|
||||
@Generable(description: "Basic profile information about a cat")
|
||||
struct CatProfile {
|
||||
var name: String
|
||||
|
||||
@Guide(description: "The age of the cat", .range(0...20))
|
||||
var age: Int
|
||||
|
||||
@Guide(description: "A one sentence profile about the cat's personality")
|
||||
var profile: String
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Request Structured Output
|
||||
|
||||
```swift
|
||||
let response = try await session.respond(
|
||||
to: "Generate a cute rescue cat",
|
||||
generating: CatProfile.self
|
||||
)
|
||||
|
||||
// Access structured fields directly
|
||||
print("Name: \(response.content.name)")
|
||||
print("Age: \(response.content.age)")
|
||||
print("Profile: \(response.content.profile)")
|
||||
```
|
||||
|
||||
### Supported @Guide Constraints
|
||||
|
||||
- `.range(0...20)` — numeric range
|
||||
- `.count(3)` — array element count
|
||||
- `description:` — semantic guidance for generation
|
||||
|
||||
## Core Pattern — Tool Calling
|
||||
|
||||
Let the model invoke custom code for domain-specific tasks:
|
||||
|
||||
### 1. Define a Tool
|
||||
|
||||
```swift
|
||||
struct RecipeSearchTool: Tool {
|
||||
let name = "recipe_search"
|
||||
let description = "Search for recipes matching a given term and return a list of results."
|
||||
|
||||
@Generable
|
||||
struct Arguments {
|
||||
var searchTerm: String
|
||||
var numberOfResults: Int
|
||||
}
|
||||
|
||||
func call(arguments: Arguments) async throws -> ToolOutput {
|
||||
let recipes = await searchRecipes(
|
||||
term: arguments.searchTerm,
|
||||
limit: arguments.numberOfResults
|
||||
)
|
||||
return .string(recipes.map { "- \($0.name): \($0.description)" }.joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Session with Tools
|
||||
|
||||
```swift
|
||||
let session = LanguageModelSession(tools: [RecipeSearchTool()])
|
||||
let response = try await session.respond(to: "Find me some pasta recipes")
|
||||
```
|
||||
|
||||
### 3. Handle Tool Errors
|
||||
|
||||
```swift
|
||||
do {
|
||||
let answer = try await session.respond(to: "Find a recipe for tomato soup.")
|
||||
} catch let error as LanguageModelSession.ToolCallError {
|
||||
print(error.tool.name)
|
||||
if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError {
|
||||
// Handle specific tool error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core Pattern — Snapshot Streaming
|
||||
|
||||
Stream structured responses for real-time UI with `PartiallyGenerated` types:
|
||||
|
||||
```swift
|
||||
@Generable
|
||||
struct TripIdeas {
|
||||
@Guide(description: "Ideas for upcoming trips")
|
||||
var ideas: [String]
|
||||
}
|
||||
|
||||
let stream = session.streamResponse(
|
||||
to: "What are some exciting trip ideas?",
|
||||
generating: TripIdeas.self
|
||||
)
|
||||
|
||||
for try await partial in stream {
|
||||
// partial: TripIdeas.PartiallyGenerated (all properties Optional)
|
||||
print(partial)
|
||||
}
|
||||
```
|
||||
|
||||
### SwiftUI Integration
|
||||
|
||||
```swift
|
||||
@State private var partialResult: TripIdeas.PartiallyGenerated?
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(partialResult?.ideas ?? [], id: \.self) { idea in
|
||||
Text(idea)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if let errorMessage { Text(errorMessage).foregroundStyle(.red) }
|
||||
}
|
||||
.task {
|
||||
do {
|
||||
let stream = session.streamResponse(to: prompt, generating: TripIdeas.self)
|
||||
for try await partial in stream {
|
||||
partialResult = partial
|
||||
}
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| On-device execution | Privacy — no data leaves the device; works offline |
|
||||
| 4,096 token limit | On-device model constraint; chunk large data across sessions |
|
||||
| Snapshot streaming (not deltas) | Structured output friendly; each snapshot is a complete partial state |
|
||||
| `@Generable` macro | Compile-time safety for structured generation; auto-generates `PartiallyGenerated` type |
|
||||
| Single request per session | `isResponding` prevents concurrent requests; create multiple sessions if needed |
|
||||
| `response.content` (not `.output`) | Correct API — always access results via `.content` property |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Always check `model.availability`** before creating a session — handle all unavailability cases
|
||||
- **Use `instructions`** to guide model behavior — they take priority over prompts
|
||||
- **Check `isResponding`** before sending a new request — sessions handle one request at a time
|
||||
- **Access `response.content`** for results — not `.output`
|
||||
- **Break large inputs into chunks** — 4,096 token limit applies to instructions + prompt + output combined
|
||||
- **Use `@Generable`** for structured output — stronger guarantees than parsing raw strings
|
||||
- **Use `GenerationOptions(temperature:)`** to tune creativity (higher = more creative)
|
||||
- **Monitor with Instruments** — use Xcode Instruments to profile request performance
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Creating sessions without checking `model.availability` first
|
||||
- Sending inputs exceeding the 4,096 token context window
|
||||
- Attempting concurrent requests on a single session
|
||||
- Using `.output` instead of `.content` to access response data
|
||||
- Parsing raw string responses when `@Generable` structured output would work
|
||||
- Building complex multi-step logic in a single prompt — break into multiple focused prompts
|
||||
- Assuming the model is always available — device eligibility and settings vary
|
||||
|
||||
## When to Use
|
||||
|
||||
- On-device text generation for privacy-sensitive apps
|
||||
- Structured data extraction from user input (forms, natural language commands)
|
||||
- AI-assisted features that must work offline
|
||||
- Streaming UI that progressively shows generated content
|
||||
- Domain-specific AI actions via tool calling (search, compute, lookup)
|
||||
279
skills/liquid-glass-design/SKILL.md
Normal file
279
skills/liquid-glass-design/SKILL.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: liquid-glass-design
|
||||
description: iOS 26 Liquid Glass design system — dynamic glass material with blur, reflection, and interactive morphing for SwiftUI, UIKit, and WidgetKit.
|
||||
---
|
||||
|
||||
# Liquid Glass Design System (iOS 26)
|
||||
|
||||
Patterns for implementing Apple's Liquid Glass — a dynamic material that blurs content behind it, reflects color and light from surrounding content, and reacts to touch and pointer interactions. Covers SwiftUI, UIKit, and WidgetKit integration.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building or updating apps for iOS 26+ with the new design language
|
||||
- Implementing glass-style buttons, cards, toolbars, or containers
|
||||
- Creating morphing transitions between glass elements
|
||||
- Applying Liquid Glass effects to widgets
|
||||
- Migrating existing blur/material effects to the new Liquid Glass API
|
||||
|
||||
## Core Pattern — SwiftUI
|
||||
|
||||
### Basic Glass Effect
|
||||
|
||||
The simplest way to add Liquid Glass to any view:
|
||||
|
||||
```swift
|
||||
Text("Hello, World!")
|
||||
.font(.title)
|
||||
.padding()
|
||||
.glassEffect() // Default: regular variant, capsule shape
|
||||
```
|
||||
|
||||
### Customizing Shape and Tint
|
||||
|
||||
```swift
|
||||
Text("Hello, World!")
|
||||
.font(.title)
|
||||
.padding()
|
||||
.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 16.0))
|
||||
```
|
||||
|
||||
Key customization options:
|
||||
- `.regular` — standard glass effect
|
||||
- `.tint(Color)` — add color tint for prominence
|
||||
- `.interactive()` — react to touch and pointer interactions
|
||||
- Shape: `.capsule` (default), `.rect(cornerRadius:)`, `.circle`
|
||||
|
||||
### Glass Button Styles
|
||||
|
||||
```swift
|
||||
Button("Click Me") { /* action */ }
|
||||
.buttonStyle(.glass)
|
||||
|
||||
Button("Important") { /* action */ }
|
||||
.buttonStyle(.glassProminent)
|
||||
```
|
||||
|
||||
### GlassEffectContainer for Multiple Elements
|
||||
|
||||
Always wrap multiple glass views in a container for performance and morphing:
|
||||
|
||||
```swift
|
||||
GlassEffectContainer(spacing: 40.0) {
|
||||
HStack(spacing: 40.0) {
|
||||
Image(systemName: "scribble.variable")
|
||||
.frame(width: 80.0, height: 80.0)
|
||||
.font(.system(size: 36))
|
||||
.glassEffect()
|
||||
|
||||
Image(systemName: "eraser.fill")
|
||||
.frame(width: 80.0, height: 80.0)
|
||||
.font(.system(size: 36))
|
||||
.glassEffect()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `spacing` parameter controls merge distance — closer elements blend their glass shapes together.
|
||||
|
||||
### Uniting Glass Effects
|
||||
|
||||
Combine multiple views into a single glass shape with `glassEffectUnion`:
|
||||
|
||||
```swift
|
||||
@Namespace private var namespace
|
||||
|
||||
GlassEffectContainer(spacing: 20.0) {
|
||||
HStack(spacing: 20.0) {
|
||||
ForEach(symbolSet.indices, id: \.self) { item in
|
||||
Image(systemName: symbolSet[item])
|
||||
.frame(width: 80.0, height: 80.0)
|
||||
.glassEffect()
|
||||
.glassEffectUnion(id: item < 2 ? "group1" : "group2", namespace: namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Morphing Transitions
|
||||
|
||||
Create smooth morphing when glass elements appear/disappear:
|
||||
|
||||
```swift
|
||||
@State private var isExpanded = false
|
||||
@Namespace private var namespace
|
||||
|
||||
GlassEffectContainer(spacing: 40.0) {
|
||||
HStack(spacing: 40.0) {
|
||||
Image(systemName: "scribble.variable")
|
||||
.frame(width: 80.0, height: 80.0)
|
||||
.glassEffect()
|
||||
.glassEffectID("pencil", in: namespace)
|
||||
|
||||
if isExpanded {
|
||||
Image(systemName: "eraser.fill")
|
||||
.frame(width: 80.0, height: 80.0)
|
||||
.glassEffect()
|
||||
.glassEffectID("eraser", in: namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button("Toggle") {
|
||||
withAnimation { isExpanded.toggle() }
|
||||
}
|
||||
.buttonStyle(.glass)
|
||||
```
|
||||
|
||||
### Extending Horizontal Scrolling Under Sidebar
|
||||
|
||||
To allow horizontal scroll content to extend under a sidebar or inspector, ensure the `ScrollView` content reaches the leading/trailing edges of the container. The system automatically handles the under-sidebar scrolling behavior when the layout extends to the edges — no additional modifier is needed.
|
||||
|
||||
## Core Pattern — UIKit
|
||||
|
||||
### Basic UIGlassEffect
|
||||
|
||||
```swift
|
||||
let glassEffect = UIGlassEffect()
|
||||
glassEffect.tintColor = UIColor.systemBlue.withAlphaComponent(0.3)
|
||||
glassEffect.isInteractive = true
|
||||
|
||||
let visualEffectView = UIVisualEffectView(effect: glassEffect)
|
||||
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||
visualEffectView.layer.cornerRadius = 20
|
||||
visualEffectView.clipsToBounds = true
|
||||
|
||||
view.addSubview(visualEffectView)
|
||||
NSLayoutConstraint.activate([
|
||||
visualEffectView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
visualEffectView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
visualEffectView.widthAnchor.constraint(equalToConstant: 200),
|
||||
visualEffectView.heightAnchor.constraint(equalToConstant: 120)
|
||||
])
|
||||
|
||||
// Add content to contentView
|
||||
let label = UILabel()
|
||||
label.text = "Liquid Glass"
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
visualEffectView.contentView.addSubview(label)
|
||||
NSLayoutConstraint.activate([
|
||||
label.centerXAnchor.constraint(equalTo: visualEffectView.contentView.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: visualEffectView.contentView.centerYAnchor)
|
||||
])
|
||||
```
|
||||
|
||||
### UIGlassContainerEffect for Multiple Elements
|
||||
|
||||
```swift
|
||||
let containerEffect = UIGlassContainerEffect()
|
||||
containerEffect.spacing = 40.0
|
||||
|
||||
let containerView = UIVisualEffectView(effect: containerEffect)
|
||||
|
||||
let firstGlass = UIVisualEffectView(effect: UIGlassEffect())
|
||||
let secondGlass = UIVisualEffectView(effect: UIGlassEffect())
|
||||
|
||||
containerView.contentView.addSubview(firstGlass)
|
||||
containerView.contentView.addSubview(secondGlass)
|
||||
```
|
||||
|
||||
### Scroll Edge Effects
|
||||
|
||||
```swift
|
||||
scrollView.topEdgeEffect.style = .automatic
|
||||
scrollView.bottomEdgeEffect.style = .hard
|
||||
scrollView.leftEdgeEffect.isHidden = true
|
||||
```
|
||||
|
||||
### Toolbar Glass Integration
|
||||
|
||||
```swift
|
||||
let favoriteButton = UIBarButtonItem(image: UIImage(systemName: "heart"), style: .plain, target: self, action: #selector(favoriteAction))
|
||||
favoriteButton.hidesSharedBackground = true // Opt out of shared glass background
|
||||
```
|
||||
|
||||
## Core Pattern — WidgetKit
|
||||
|
||||
### Rendering Mode Detection
|
||||
|
||||
```swift
|
||||
struct MyWidgetView: View {
|
||||
@Environment(\.widgetRenderingMode) var renderingMode
|
||||
|
||||
var body: some View {
|
||||
if renderingMode == .accented {
|
||||
// Tinted mode: white-tinted, themed glass background
|
||||
} else {
|
||||
// Full color mode: standard appearance
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accent Groups for Visual Hierarchy
|
||||
|
||||
```swift
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Title")
|
||||
.widgetAccentable() // Accent group
|
||||
Text("Subtitle")
|
||||
// Primary group (default)
|
||||
}
|
||||
Image(systemName: "star.fill")
|
||||
.widgetAccentable() // Accent group
|
||||
}
|
||||
```
|
||||
|
||||
### Image Rendering in Accented Mode
|
||||
|
||||
```swift
|
||||
Image("myImage")
|
||||
.widgetAccentedRenderingMode(.monochrome)
|
||||
```
|
||||
|
||||
### Container Background
|
||||
|
||||
```swift
|
||||
VStack { /* content */ }
|
||||
.containerBackground(for: .widget) {
|
||||
Color.blue.opacity(0.2)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| GlassEffectContainer wrapping | Performance optimization, enables morphing between glass elements |
|
||||
| `spacing` parameter | Controls merge distance — fine-tune how close elements must be to blend |
|
||||
| `@Namespace` + `glassEffectID` | Enables smooth morphing transitions on view hierarchy changes |
|
||||
| `interactive()` modifier | Explicit opt-in for touch/pointer reactions — not all glass should respond |
|
||||
| UIGlassContainerEffect in UIKit | Same container pattern as SwiftUI for consistency |
|
||||
| Accented rendering mode in widgets | System applies tinted glass when user selects tinted Home Screen |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Always use GlassEffectContainer** when applying glass to multiple sibling views — it enables morphing and improves rendering performance
|
||||
- **Apply `.glassEffect()` after** other appearance modifiers (frame, font, padding)
|
||||
- **Use `.interactive()`** only on elements that respond to user interaction (buttons, toggleable items)
|
||||
- **Choose spacing carefully** in containers to control when glass effects merge
|
||||
- **Use `withAnimation`** when changing view hierarchies to enable smooth morphing transitions
|
||||
- **Test across appearances** — light mode, dark mode, and accented/tinted modes
|
||||
- **Ensure accessibility contrast** — text on glass must remain readable
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Using multiple standalone `.glassEffect()` views without a GlassEffectContainer
|
||||
- Nesting too many glass effects — degrades performance and visual clarity
|
||||
- Applying glass to every view — reserve for interactive elements, toolbars, and cards
|
||||
- Forgetting `clipsToBounds = true` in UIKit when using corner radii
|
||||
- Ignoring accented rendering mode in widgets — breaks tinted Home Screen appearance
|
||||
- Using opaque backgrounds behind glass — defeats the translucency effect
|
||||
|
||||
## When to Use
|
||||
|
||||
- Navigation bars, toolbars, and tab bars with the new iOS 26 design
|
||||
- Floating action buttons and card-style containers
|
||||
- Interactive controls that need visual depth and touch feedback
|
||||
- Widgets that should integrate with the system's Liquid Glass appearance
|
||||
- Morphing transitions between related UI states
|
||||
216
skills/swift-concurrency-6-2/SKILL.md
Normal file
216
skills/swift-concurrency-6-2/SKILL.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
name: swift-concurrency-6-2
|
||||
description: Swift 6.2 Approachable Concurrency — single-threaded by default, @concurrent for explicit background offloading, isolated conformances for main actor types.
|
||||
---
|
||||
|
||||
# Swift 6.2 Approachable Concurrency
|
||||
|
||||
Patterns for adopting Swift 6.2's concurrency model where code runs single-threaded by default and concurrency is introduced explicitly. Eliminates common data-race errors without sacrificing performance.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Migrating Swift 5.x or 6.0/6.1 projects to Swift 6.2
|
||||
- Resolving data-race safety compiler errors
|
||||
- Designing MainActor-based app architecture
|
||||
- Offloading CPU-intensive work to background threads
|
||||
- Implementing protocol conformances on MainActor-isolated types
|
||||
- Enabling Approachable Concurrency build settings in Xcode 26
|
||||
|
||||
## Core Problem: Implicit Background Offloading
|
||||
|
||||
In Swift 6.1 and earlier, async functions could be implicitly offloaded to background threads, causing data-race errors even in seemingly safe code:
|
||||
|
||||
```swift
|
||||
// Swift 6.1: ERROR
|
||||
@MainActor
|
||||
final class StickerModel {
|
||||
let photoProcessor = PhotoProcessor()
|
||||
|
||||
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
|
||||
|
||||
// Error: Sending 'self.photoProcessor' risks causing data races
|
||||
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Swift 6.2 fixes this: async functions stay on the calling actor by default.
|
||||
|
||||
```swift
|
||||
// Swift 6.2: OK — async stays on MainActor, no data race
|
||||
@MainActor
|
||||
final class StickerModel {
|
||||
let photoProcessor = PhotoProcessor()
|
||||
|
||||
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
|
||||
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core Pattern — Isolated Conformances
|
||||
|
||||
MainActor types can now conform to non-isolated protocols safely:
|
||||
|
||||
```swift
|
||||
protocol Exportable {
|
||||
func export()
|
||||
}
|
||||
|
||||
// Swift 6.1: ERROR — crosses into main actor-isolated code
|
||||
// Swift 6.2: OK with isolated conformance
|
||||
extension StickerModel: @MainActor Exportable {
|
||||
func export() {
|
||||
photoProcessor.exportAsPNG()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The compiler ensures the conformance is only used on the main actor:
|
||||
|
||||
```swift
|
||||
// OK — ImageExporter is also @MainActor
|
||||
@MainActor
|
||||
struct ImageExporter {
|
||||
var items: [any Exportable]
|
||||
|
||||
mutating func add(_ item: StickerModel) {
|
||||
items.append(item) // Safe: same actor isolation
|
||||
}
|
||||
}
|
||||
|
||||
// ERROR — nonisolated context can't use MainActor conformance
|
||||
nonisolated struct ImageExporter {
|
||||
var items: [any Exportable]
|
||||
|
||||
mutating func add(_ item: StickerModel) {
|
||||
items.append(item) // Error: Main actor-isolated conformance cannot be used here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core Pattern — Global and Static Variables
|
||||
|
||||
Protect global/static state with MainActor:
|
||||
|
||||
```swift
|
||||
// Swift 6.1: ERROR — non-Sendable type may have shared mutable state
|
||||
final class StickerLibrary {
|
||||
static let shared: StickerLibrary = .init() // Error
|
||||
}
|
||||
|
||||
// Fix: Annotate with @MainActor
|
||||
@MainActor
|
||||
final class StickerLibrary {
|
||||
static let shared: StickerLibrary = .init() // OK
|
||||
}
|
||||
```
|
||||
|
||||
### MainActor Default Inference Mode
|
||||
|
||||
Swift 6.2 introduces a mode where MainActor is inferred by default — no manual annotations needed:
|
||||
|
||||
```swift
|
||||
// With MainActor default inference enabled:
|
||||
final class StickerLibrary {
|
||||
static let shared: StickerLibrary = .init() // Implicitly @MainActor
|
||||
}
|
||||
|
||||
final class StickerModel {
|
||||
let photoProcessor: PhotoProcessor
|
||||
var selection: [PhotosPickerItem] // Implicitly @MainActor
|
||||
}
|
||||
|
||||
extension StickerModel: Exportable { // Implicitly @MainActor conformance
|
||||
func export() {
|
||||
photoProcessor.exportAsPNG()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This mode is opt-in and recommended for apps, scripts, and other executable targets.
|
||||
|
||||
## Core Pattern — @concurrent for Background Work
|
||||
|
||||
When you need actual parallelism, explicitly offload with `@concurrent`:
|
||||
|
||||
> **Important:** This example requires Approachable Concurrency build settings — SE-0466 (MainActor default isolation) and SE-0461 (NonisolatedNonsendingByDefault). With these enabled, `extractSticker` stays on the caller's actor, making mutable state access safe. **Without these settings, this code has a data race** — the compiler will flag it.
|
||||
|
||||
```swift
|
||||
nonisolated final class PhotoProcessor {
|
||||
private var cachedStickers: [String: Sticker] = [:]
|
||||
|
||||
func extractSticker(data: Data, with id: String) async -> Sticker {
|
||||
if let sticker = cachedStickers[id] {
|
||||
return sticker
|
||||
}
|
||||
|
||||
let sticker = await Self.extractSubject(from: data)
|
||||
cachedStickers[id] = sticker
|
||||
return sticker
|
||||
}
|
||||
|
||||
// Offload expensive work to concurrent thread pool
|
||||
@concurrent
|
||||
static func extractSubject(from data: Data) async -> Sticker { /* ... */ }
|
||||
}
|
||||
|
||||
// Callers must await
|
||||
let processor = PhotoProcessor()
|
||||
processedPhotos[item.id] = await processor.extractSticker(data: data, with: item.id)
|
||||
```
|
||||
|
||||
To use `@concurrent`:
|
||||
1. Mark the containing type as `nonisolated`
|
||||
2. Add `@concurrent` to the function
|
||||
3. Add `async` if not already asynchronous
|
||||
4. Add `await` at call sites
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Single-threaded by default | Most natural code is data-race free; concurrency is opt-in |
|
||||
| Async stays on calling actor | Eliminates implicit offloading that caused data-race errors |
|
||||
| Isolated conformances | MainActor types can conform to protocols without unsafe workarounds |
|
||||
| `@concurrent` explicit opt-in | Background execution is a deliberate performance choice, not accidental |
|
||||
| MainActor default inference | Reduces boilerplate `@MainActor` annotations for app targets |
|
||||
| Opt-in adoption | Non-breaking migration path — enable features incrementally |
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. **Enable in Xcode**: Swift Compiler > Concurrency section in Build Settings
|
||||
2. **Enable in SPM**: Use `SwiftSettings` API in package manifest
|
||||
3. **Use migration tooling**: Automatic code changes via swift.org/migration
|
||||
4. **Start with MainActor defaults**: Enable inference mode for app targets
|
||||
5. **Add `@concurrent` where needed**: Profile first, then offload hot paths
|
||||
6. **Test thoroughly**: Data-race issues become compile-time errors
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Start on MainActor** — write single-threaded code first, optimize later
|
||||
- **Use `@concurrent` only for CPU-intensive work** — image processing, compression, complex computation
|
||||
- **Enable MainActor inference mode** for app targets that are mostly single-threaded
|
||||
- **Profile before offloading** — use Instruments to find actual bottlenecks
|
||||
- **Protect globals with MainActor** — global/static mutable state needs actor isolation
|
||||
- **Use isolated conformances** instead of `nonisolated` workarounds or `@Sendable` wrappers
|
||||
- **Migrate incrementally** — enable features one at a time in build settings
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Applying `@concurrent` to every async function (most don't need background execution)
|
||||
- Using `nonisolated` to suppress compiler errors without understanding isolation
|
||||
- Keeping legacy `DispatchQueue` patterns when actors provide the same safety
|
||||
- Skipping `model.availability` checks in concurrency-related Foundation Models code
|
||||
- Fighting the compiler — if it reports a data race, the code has a real concurrency issue
|
||||
- Assuming all async code runs in the background (Swift 6.2 default: stays on calling actor)
|
||||
|
||||
## When to Use
|
||||
|
||||
- All new Swift 6.2+ projects (Approachable Concurrency is the recommended default)
|
||||
- Migrating existing apps from Swift 5.x or 6.0/6.1 concurrency
|
||||
- Resolving data-race safety compiler errors during Xcode 26 adoption
|
||||
- Building MainActor-centric app architectures (most UI apps)
|
||||
- Performance optimization — offloading specific heavy computations to background
|
||||
Reference in New Issue
Block a user