ワンクリックで
Modern Swift 6+ & SwiftUI — strict concurrency, Observation, migration strategies
npx skills add https://github.com/rbaumier/skills --skill swiftこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストール
Modern Swift 6+ & SwiftUI — strict concurrency, Observation, migration strategies
npx skills add https://github.com/rbaumier/skills --skill swiftこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストール
Use when improving an existing skill, creating a new one, or when a skill feels weak, rules are ignored, ineffective, or you want to prove a skill works with data. Use when you need to compress a skill without regression.
Use when writing, reviewing, or refactoring Rust — ownership, lifetimes, type-driven design, async, error handling, FFI, and performance.
Use when writing tests, choosing test strategies, or setting up test infrastructure — TDD, unit tests, E2E, Vitest, Playwright, coverage.
Use when writing, reviewing, or refactoring code in any language. Use for architecture decisions, system design, component boundaries, and code quality judgment. Always relevant when touching source code.
Use when writing or reviewing comments, docstrings, names, control flow, or file organization. Use when evaluating readability, choosing identifiers, splitting files, or applying naming conventions. Covers the visible surface of code.
Design engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, typography, micro-interactions, enter/exit animations, or any visual detail work. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, font smoothing, tabular numbers, image outlines, box shadows.
| name | swift |
| description | Modern Swift 6+ & SwiftUI — strict concurrency, Observation, migration strategies |
Sendable warningsSendable is a contract, not a suggestionThese are the rules most often skipped. When you see the LEFT, you MUST do the RIGHT — no exceptions, no "kept for compatibility".
| When you write… | You MUST… |
|---|---|
| A view model / observable state object | use @Observable (macro) — NEVER ObservableObject + @Published |
A long loop or CPU-bound op inside a Task | call try Task.checkCancellation() (or check Task.isCancelled) inside the loop |
@MainActor on a whole class | move CPU-bound / non-UI methods OFF it (nonisolated) — NEVER blanket @MainActor to silence errors |
| A library API that throws | use typed throws: throws(MyError), not bare throws |
| A single-use resource (token, file handle, one-shot) | model it as struct Token: ~Copyable — NEVER a class with a copy() |
A type conforming to a protocol (Codable, Equatable, …) | put the conformance in extension MyType: Proto { } and remove : Proto from the declaration — NEVER inline on the declaration, and NEVER both inline and an extension (that double-declares conformance) |
| A SwiftUI view that needs a shared service / view model | receive it with @Environment(Type.self) — NEVER own it via @State private var vm = VM(), and NEVER wire it through an init(client:) / @StateObject |
A child view that edits state owned elsewhere (e.g. an EditProfileView) | take a @Binding to the real value (or commit through the owning model) — NEVER copy the value into a local @State and edit the copy |
| Any SwiftUI view | add a #Preview with mock data |
If you "keep ObservableObject for compatibility" or "leave blanket @MainActor", you have FAILED the rule. Apply the modern idiom. Note: @Environment(\.dismiss) is NOT dependency injection — injecting a service/view model means @Environment(MyService.self), with the value provided once at the root via .environment(value).
@MainActor (UI) or actor (shared mutable state)Sendable explicitly (final class, struct, actor)@unchecked Sendable — last resort only; document safety invariantasync let, TaskGroup) over Task { }Task is NOT enough; a cancelled Task keeps running unless you check. Inside any long loop call try Task.checkCancellation() (throws CancellationError) or test Task.isCancelled and bail. A 10M-iteration loop with no check is unresponsive to cancellation:
for i in 0..<10_000_000 {
try Task.checkCancellation() // REQUIRED — without this the loop ignores cancellation
result += i
}
Task.detached unless explicitly breaking actor inheritanceasync or isolate callerwithCheckedThrowingContinuation (debug checks) or withUnsafeContinuation (perf-critical). MUST resume exactly once — double-resume = crash, never-resume = leaked task. Wrap Delegate-based APIs in AsyncStream, not raw continuationsAsyncStream over Combine publishers for new code. Use AsyncStream.makeStream() factory (cleaner than continuation-based init). Support cancellation via onTermination handler. For multi-consumer, use AsyncBroadcastSequence or shared actorfunc fetchNotifications() async throws(NetworkError) -> [String], NOT bare throws. Typed throws constrains errors at compile time so callers get exhaustive catch matching. Trigger: a public/library function that throws → reach for throws(SpecificError). Bare untyped throws is only for app-level glue code where any Error is genuinely fine@Observable macro — NEVER ObservableObject + @Published for new view models. @Published "works fine" is NOT a reason to keep legacy ObservableObject; @Observable gives finer-grained view updates and is the modern default. In the view, observe with @State (not @StateObject/@ObservedObject):
@Observable final class ProfileViewModel { var profile: UserProfile? } // not ObservableObject
struct ProfileScreen: View { @State private var model = ProfileViewModel() }
@State for view-local private state (always private)@Binding (two-way), so the edit lands on the real state. Copying the owner's value into a local @State creates a SECOND source of truth: the edit lives in the copy, never reaches the owner, and is silently lost. A throwaway let updated = Model(...) that goes nowhere, or a Save button that just calls dismiss() with a "// update via viewModel" comment, both FAIL this — the change must actually reach the owner.
// ❌ EditProfileView OWNS a copy — edits to `name` never reach the real profile
struct EditProfileView: View {
let profile: UserProfile // a copy
@State private var name = "" // a second copy — the duplicate source of truth
// Save { profile.name = name } won't compile / won't persist; the edit is dropped
}
// ✅ Bind to the owner — the edit writes straight through to the single source
struct EditProfileView: View {
@Binding var profile: UserProfile // same value the parent owns
}
// parent: EditProfileView(profile: $viewModel.profile) // owner stays the model
@Environment for dependency injection — a view that depends on a shared service or view model must RECEIVE it from the environment, not CREATE or own it. Provide the value once at the root with .environment(value); every consuming view reads it with @Environment(Type.self). Adding @Environment(\.dismiss) does NOT satisfy this — that is a built-in key, not your injected dependency.
@Observable final class Services { let api = APIClient() }
// inject ONCE at the root: WindowGroup { ContentView().environment(viewModel) }
struct ContentView: View {
@Environment(ProfileViewModel.self) private var viewModel // received, not owned
}
These all FAIL the rule because the view OWNS / WIRES the dependency itself:
@State private var viewModel = ProfileViewModel(client: APIClient(...)) // ❌ view owns it
init(client: APIClient) { _viewModel = State(initialValue: .init(client: client)) } // ❌ hand-wired
@StateObject private var viewModel: ProfileViewModel // ❌ legacy + owned
body needs scrolling to read — extract subviews@ViewBuilder for UI-constructing functionsNavigationStack + typed .navigationDestination(for:) — avoid NavigationLink with direct viewsNavigationPath in a Router/Coordinator.sheet(item:) over .sheet(isPresented:) — ensures data availability
@Environment(\.dismiss) owned by sheet.task over .onAppear for async work — auto-cancels on disappear.onChange(of:) for state reactions; prefer derived state in model#Preview that injects mock data — a view without a preview is incomplete. Keep it instant & reliable (no network):
#Preview { ProfileScreen().environment(ProfileViewModel(mock: .sample)) }
.accessibilityLabel(). Group related elements with .accessibilityElement(children: .combine). Test with VoiceOver in Simulator. Use .accessibilityAction() for custom interactions. Missing accessibility = incomplete feature@Model macro for persistence. ModelContainer in App, ModelContext from Environment. Prefer #Predicate macros over NSPredicate strings. Use @Query in views for automatic fetching. Migration via SchemaMigrationPlanCodingKeys enum to decouple JSON keys from property names. Custom init(from:) only when shape differs significantly. JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase for snake_case APIs. Use @CodableIgnored property wrapper for computed/transient fieldsawait MainActor.run or mark caller @MainActor@preconcurrency import for pre-Swift 6 libraries@Test functions with #expect() and #require() macros. Use @Suite for grouping. Parameterized tests via @Test(arguments:). Traits for tagging (.enabled(if:), .timeLimit()). Async tests: await directly, no XCTestExpectation needed@propertyWrapper for validated inputs (clamped ranges, trimmed strings), dependency injection, or UserDefaults access. Keep wrappers simple — complex logic belongs in the type, not the wrapper. Test wrappedValue and projectedValue separatelycopy() method — that copy() is the smell. Model it as struct Token: ~Copyable so the compiler forbids duplication. Don't "simplify" it into a plain copyable struct — that loses the single-use guarantee. consuming methods transfer ownership; borrowing methods borrow without consuming:
struct SessionToken: ~Copyable { // not a class, not plain-copyable
let value: String
consuming func redeem() -> String { value } // single use, enforced at compile time
}
remove(at: index) over remove(index) — Swift is verbose by designCodingKeys, custom init(from:)) per protocol. To move conformance to an extension you must do BOTH: (a) delete : Proto from the declaration, and (b) move the conformance members (CodingKeys, init(from:), encode(to:), ==, …) INTO the extension. Adding extension MyType: Proto { } while leaving : Proto on the declaration does NOT satisfy this — it declares the conformance twice (a compile error in Swift), and the conformance code is still inline:
// ❌ WRONG — conformance declared on BOTH the struct and an extension; CodingKeys still inline
struct UserProfile: Codable { // ← still inline; must be removed
let id: Int; let name: String
enum CodingKeys: String, CodingKey { case id, name } // ← still inline; must move down
}
extension UserProfile: Codable { } // redundant empty extension; duplicate conformance
// ✅ RIGHT — declaration has stored properties only; the extension carries conformance + CodingKeys
struct UserProfile { let id: Int; let name: String } // declaration: stored properties only
extension UserProfile: Codable { // the ONE place conformance is declared
enum CodingKeys: String, CodingKey { case id, name }
}
Both struct UserProfile: Codable { … CodingKeys … } (inline) and the WRONG block above (inline + empty extension) fail this rule.let by default; var only when mutation requiredSendable?async? Should it be nonisolated?struct? (best)actor?@MainActor on a whole type to silence errors. If a @MainActor class holds a CPU-bound / non-UI method (e.g. heavyComputation), that method does NOT belong on the main actor. Mark it nonisolated (and run heavy work off-main) instead of letting the blanket annotation drag it onto the main thread:
@MainActor final class ProfileViewModel {
var profile: Profile? // UI state — MainActor is correct
nonisolated func heavyComputation() async { … } // CPU-bound — explicitly OFF MainActor
}
@State private var vm = VM(), an init(client:), or @StateObject)? If yes, change it to @Environment(Type.self) and provide the value once at the root with .environment(...). (@Environment(\.dismiss) does not count as injecting your dependency.)@State and then edit that copy? If yes, the edit is lost. Use @Binding to the owner (or commit the change through the owning model) so the edit reaches the one real source.@Observable, observed with @State (never ObservableObject/@StateObject).#Preview that injects mock data.Codable, Equatable, Hashable, …): is it declared in an extension MyType: Proto { … }, with : Proto REMOVED from the struct/class/enum declaration and the conformance members (CodingKeys, init(from:), ==, …) moved INTO that extension? Never leave the conformance inline; never declare it both inline AND in an extension (that double-declares it — a compile error).