swift-programmer skill
Provides expertise in Swift programming idioms, protocol-oriented design, and Swift 6 strict concurrency models for app and script development.
- GitHub stars
- 9
- Files
- 3
- Last commit
- Oct 13, 2026
- License
- EPL-2.0
- Allowed tools
- —
What does the swift-programmer skill do?
swift-programmer provides expertise in Swift programming idioms, protocol-oriented design, and Swift 6 strict concurrency models for app and script development.
Summary generated from the skill's SKILL.md.
About the swift-programmer skill
Developing Swift applications or command-line scripts with a focus on safety and modern idioms is what this skill enables. It provides deep expertise in Swift 6 strict concurrency models, helping developers move away from manual thread management toward isolation domains. By utilizing tools like actors, the main actor, and structured concurrency through TaskGroups or async let, it ensures code is data-race safe at compile time.
Developers use this skill to implement protocol-oriented design, value semantics, and static dispatch. It is particularly useful when navigating the complexities of actor re-entrancy or managing Sendable conformance for types shared across isolation boundaries. The skill guides the selection of appropriate isolation strategies, such as using @MainActor for SwiftUI models or custom actors for shared mutable state, ensuring that the compiler can prove the correctness of the implementation.
What an agent can do with it
- 1Implement Swift 6 concurrencyRefactor existing code to use isolation domains and structured concurrency to ensure data-race safety.
- 2Manage SwiftUI state isolationApply the correct actor isolation to view models to prevent threading issues in SwiftUI applications.
- 3Design thread-safe data structuresCreate types that are safe to share across different tasks using Sendable requirements.
- 4Handle actor re-entrancy safelyWrite actor methods that maintain internal consistency even when suspended at an await point.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the swift-programmer skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the swift-programmer skill in Plugin
/plugin marketplace add Pyroxin/opinionated-claude-skills /plugin install opinionated-apple-development@opinionated-claude-skills
Install the swift-programmer skill in Claude Code
~/.claude/skills/swift-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/swift-programmer ~/.claude/skills/swift-programmerInstall the swift-programmer skill in Claude Code (project)
.claude/skills/swift-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/swift-programmer .claude/skills/swift-programmerInstall the swift-programmer skill in Codex
~/.agents/skills/swift-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/swift-programmer ~/.agents/skills/swift-programmerInstall the swift-programmer skill in Gemini CLI
~/.gemini/skills/swift-programmer/SKILL.md
$ npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/swift-programmer ~/.gemini/skills/swift-programmerInstall the swift-programmer skill in Claude.ai
npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/swift-programmer swift-programmer && zip -r swift-programmer.zip swift-programmer # claude.ai → Customize → Skills → + → Create skill → Upload a skill → swift-programmer.zip
SKILL.md
View on GitHubSwift Programming
<skill_scope skill="swift-programmer"> Related skills:
software-engineer— Core design principles and system architecturemacos-programmer— macOS-specific patterns when building Mac appstest-driven-development— Testing philosophy and practicesfunctional-programmer— Functional paradigm principles (Swift supports FP)fish-shell-scripting— Shell-scripting alternative when Swift's framework access isn't needed
This skill covers Swift-specific idioms, tooling, and philosophy for both application development and command-line scripting. It emphasizes protocol-oriented programming, value semantics, strict concurrency (Swift 6+), and compile-time safety guarantees. </skill_scope>
Core Philosophy
<core_philosophy> This section states the defaults this skill applies when writing Swift; the sections that follow give the judgment frameworks for the cases where a default doesn't fit.
Let the compiler carry the safety argument. Swift 6 adds compile-time data-race safety to memory safety, so design so that the checker can prove the code correct rather than suppressing its diagnostics; the working model for that — isolation domains rather than threads — is developed in <concurrency_fundamentals>.
Prefer protocol composition over inheritance, value semantics over reference semantics, and static dispatch over dynamic dispatch.
Version targeting: Use the latest Swift version available. For internal apps, target only the current version. For open-source libraries, support at most the current version and one or two prior ones. </core_philosophy>
Swift 6 Concurrency
<concurrency_fundamentals>
Think in isolation domains, not threads. Each domain — a task, an actor, or a global actor such as the main actor — executes serially with exclusive access to its state, and every piece of mutable state belongs to exactly one domain at a time. An await marks a possible suspension point: the task may give up its thread there, and on resumption it may run on a different thread but always in the same isolation domain. Write the code after an await to tolerate suspension without depending on it having happened. The compiler reasons about logical isolation, not physical threading, so design for the former.
Prefer structured concurrency. Child tasks (async let, TaskGroup) inherit their parent's priority and cancellation and cannot outlive it. SE-0304 frames unstructured tasks — both Task { } and Task.detached — as the escape valve for work "whose lifetime is not bound to the creating task, for example in order to fire-and-forget some operation or to initiate asynchronous work from synchronous code."[^se-0304] The two differ in what they inherit: Task.detached inherits no actor isolation, priority, or task-local values, whereas Task { } inherits all three.[^tspl] Use Task { } for unstructured work that should keep its context, and Task.detached only when independence from that context is the point.
Approachable Concurrency (opt-in, Swift 6.2+)[^approachable-concurrency]: a configuration pairing SE-0461's caller's-actor default for nonisolated async functions[^se-0461] with SE-0466's default @MainActor inference.[^se-0466] Under it, unannotated code is inferred @MainActor and a nonisolated async function runs on whichever actor called it, so code that doesn't opt into parallelism runs sequentially and most data-race errors for naturally sequential code disappear. Parallelism still enters where you ask for it — @concurrent, Task.detached, a custom actor, or a concurrent API — and only the unannotated and nonisolated async cases change; explicitly isolated code behaves as before. Apple states that main-actor mode "is enabled by default for new app projects created with Xcode 26";[^wwdc25-268] existing targets and bare -swift-version 6 builds do not enable it, and swift package init generates a manifest with the Swift 6 language mode but neither setting (observed with Swift 6.3.2; check the generated Package.swift on your toolchain). Check the target's settings before assuming either execution behavior.
</concurrency_fundamentals>
Choosing an Isolation Strategy
<isolation_decision>
Apple's own guidance sets the default: the main actor is "the most common way to protect global state," annotating a whole class @MainActor is common "especially in a project that doesn't have a lot of concurrent tasks," and default main-actor isolation is "recommended for apps, scripts, and other executable targets."[^apple-concurrency-updates] Reach for a custom actor when state is genuinely shared by concurrent work that runs off the main actor. The following table covers the common situations; it is not a complete list:
| Situation | Use | Why |
|---|---|---|
| UI state, SwiftUI observable models | @MainActor class (with @Observable) |
SwiftUI reads state on the main actor; Apple DTS guidance is that view models "make sense … main-actor bound"[^dts-798211] |
| Mostly single-threaded target (app, script, tool) | -default-isolation MainActor (Xcode: Default Actor Isolation) |
Infers @MainActor project-wide, with concurrency encapsulated where you opt in[^apple-concurrency-updates] |
| Mutable state shared by parallel workers | actor |
A custom serialization domain; callers pay an await |
| Global or static mutable state | @MainActor on the declaration or its type, or let of a Sendable type |
SE-0412 requires every global to be actor-isolated or immutable-and-Sendable[^se-0412] |
| No mutable shared state | struct, or a final class with only let properties |
Nothing to isolate |
| CPU-bound work that must not stall an actor | @concurrent async function |
Runs on the concurrent pool and frees the actor (see <offloading_with_concurrent>) |
Prefer @MainActor over a custom actor for anything SwiftUI observes: views run on the main actor and would need an await to read a custom actor's state, which is exactly the friction main-actor isolation removes.
</isolation_decision>
Actor Re-entrancy
<actor_reentrancy>
Actors are re-entrant. SE-0306 states that "actor-isolated state can change across an await when an interleaved task mutates that state, meaning that developers must be sure not to break invariants across an await."[^se-0306] Apple's guidance for designing around this:[^wwdc21-10133] perform mutation of actor state within synchronous code, restore consistency before every await, and re-check any assumption about actor or global state after the await resumes.
actor OrderQueue {
var pending: [Order] = []
var inFlight: Set<Order.ID> = []
func processNext() async {
guard let order = pending.first(where: { !inFlight.contains($0.id) }) else { return }
inFlight.insert(order.id) // claim before suspending: a second caller cannot pick the same order
await perform(order) // other tasks may run here and mutate `pending`
inFlight.remove(order.id)
if let i = pending.firstIndex(where: { $0.id == order.id }) {
pending.remove(at: i) // re-validate: `pending` may have changed across the await
} // claim, and check-and-remove, each contain no `await`, so neither can be interleaved
}
}
The claim is what prevents duplicate work: without inFlight, two callers suspended in perform would both have read the same first element. Keep actor methods synchronous where you can and compose async wrappers around them; a synchronous actor method cannot be interleaved.
</actor_reentrancy>
@MainActor Guarantees
<mainactor_guarantees>
@MainActor guarantees main-thread execution for async functions unconditionally, and for synchronous functions only when they are called from a main-actor context. Calling a synchronous @MainActor function from a synchronous nonisolated context is a compile-time error in both language modes (diagnostic ActorIsolatedCall); the fixes are to isolate the caller to the main actor or to wrap the call in Task { @MainActor in … }.[^apple-diagnostics] Where the two modes differ is at @preconcurrency boundaries, e.g., a @MainActor type conforming to a @preconcurrency protocol: the Swift 5 language mode warns ("this is an error in the Swift 6 language mode", diagnostic ConformanceIsolation) and the callback can arrive off the main thread at run time, while the Swift 6 language mode rejects the conformance (compiler output observed with Swift 6.3.2; the bundled conformance-isolation.md describes the fix, not the mode difference).[^apple-diagnostics] SE-0423 offers a third option: put @preconcurrency on the conformance itself (final class C: @preconcurrency P), which the Swift 6 language mode accepts and enforces at run time — a requirement called off the actor "will result in a runtime error that halts program execution."[^se-0423] Use it for protocols whose off-actor calls would be a bug in the caller, not as a way to silence the diagnostic.
Two Swift 6.2 additions reduce how much annotation main-actor code needs:
- Isolated conformances — a conformance that needs main-actor state is written
extension Model: @MainActor Exportable, and the compiler then permits its use only from main-actor contexts.[^apple-concurrency-updates] - Default main-actor mode —
-default-isolation MainActorinfers@MainActoron declarations project-wide; opt individual functions out withnonisolatedor@concurrent.[^se-0466]
Mark a method nonisolated when it touches no main-actor state and must be callable from anywhere; the compiler enforces that it accesses no isolated state. In the Swift 6 language mode Thread.isMainThread is unavailable from async contexts — the compiler steers you toward @MainActor annotations rather than thread checks.
</mainactor_guarantees>
Sendable
<sendable_rules>
Sendable marks a type as safe to share across isolation domains. The following kinds qualify (Apple's Sendable documentation states the requirements[^apple-sendable]):
- value types whose stored properties are all
Sendable; finalclasses whose stored properties are all immutable andSendable, with no superclass other thanNSObject;- actors, implicitly;
- functions and closures marked
@Sendable, which may capture onlySendablevalues and no mutable variables.
Implicit conformance is narrow: Apple's Sendable documentation grants it to structures and enumerations that qualify and are either frozen or non-public and not @usableFromInline, and SE-0316 grants it to any non-protocol type annotated with a global actor (e.g., a @MainActor class),[^se-0316] with SE-0434's qualifications: a global-actor-isolated subclass of a non-Sendable superclass is not Sendable, and a global-actor-isolated closure may capture non-Sendable values even though it is @Sendable, because it can never run concurrently with itself.[^se-0434] Other classes, and public non-frozen value types without global-actor isolation, declare the conformance explicitly. A @Sendable closure that needs a mutable variable's current value captures it by value in the capture list ({ [count] in … }).[^apple-diagnostics]
Escape hatches, in order of preference. The migration guide's rule is that "if a type isn't already thread-safe, attempting to make it Sendable should not be your first approach";[^migration-common-problems] reach for an actor or @MainActor first. When a type genuinely does its own synchronization, prefer a form the compiler can check: Mutex from the Synchronization module is itself Sendable,[^se-0433] so a final class whose state lives entirely in let Mutex properties conforms to Sendable with checking intact. Reserve @unchecked Sendable for synchronization the compiler cannot see (e.g., a lock guarding a var), and document the invariant it relies on. For a single variable guarded by an external lock or queue, nonisolated(unsafe) opts that one declaration out of checking; the guide restricts it to cases where "you are carefully guarding all access to the variable with an external synchronization mechanism."[^migration-common-problems]
</sendable_rules>
Region-Based Isolation and sending
<region_isolation>
Region-based isolation (SE-0414) lets a non-Sendable value cross an isolation boundary when the compiler can prove that the value's isolation region — the value and everything it may alias — is not used by the sending side while the receiver may still be using it: a value sent into an actor's persistent state is unusable by the sender from then on, whereas one passed as an ordinary (non-sending) parameter to a nonisolated async function becomes usable again when that call returns;[^se-0414] a sending parameter permits the callee to transfer the value onward, so the caller cannot use it after the call regardless of return; that modifier (SE-0430) is how an API states the transfer explicitly in its signature.[^se-0430] Use sending when an API hands ownership of a non-Sendable object across actors and you want the transfer checked rather than forced through a Sendable conformance. The diagnostics sending-risks-data-race.md and sending-closure-risks-data-race.md explain the failure cases.[^apple-diagnostics]
</region_isolation>
Offloading Work with @concurrent
<offloading_with_concurrent>
Under NonisolatedNonsendingByDefault, a nonisolated async function runs on whichever actor called it; @concurrent marks a function that "must always switch off of an actor to run."[^apple-nonisolated-nonsending] The Approachable Concurrency vision frames the decision as the last stage of a progression — start on the main actor, and "when the programmer is ready to embrace concurrency to get better performance, they can explicitly offload work from the main actor to the cooperative thread pool."[^approachable-concurrency]
Apple's recipe for offloading a function:[^apple-concurrency-updates] add @concurrent to the function (which makes that function nonisolated), make it async if it isn't already, and await it at call sites; Apple's worked example also marks the enclosing type nonisolated, but do that only when none of the type's members need actor isolation, because SE-0461 permits @concurrent methods inside actors and @MainActor types.[^se-0461] Apply it to CPU-intensive work (e.g., image or video processing, large parses) that would otherwise stall the calling actor; I/O that suspends rather than blocks gains nothing from it, unless the same function also does substantial synchronous work after the await (e.g., decoding a large response), in which case that work is what you offload. Before the feature is enabled, nonisolated(nonsending) gives an individual function the caller's-actor behavior, and the migration flag -enable-upcoming-feature NonisolatedNonsendingByDefault:migrate adds @concurrent to existing nonisolated async functions so their semantics don't change silently.[^apple-nonisolated-nonsending]
</offloading_with_concurrent>
Migrating to Strict Concurrency
<migration_diagnostics>
Migrate incrementally. The migration guide describes moving "towards the Swift 6 language mode … in stages," introducing concurrency features gradually and addressing problems as they surface,[^migration-incremental] with @preconcurrency import as the staging tool for dependencies that have not migrated (it suppresses or downgrades to warnings the Sendable diagnostics that involve that module's types, depending on the case and language mode).[^se-0337] Xcode bundles a document for each strict-concurrency diagnostic under /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/doc/swift/diagnostics/;[^apple-diagnostics] read the matching file when an error appears. The following are the ones that come up most, not a complete list:
| Diagnostic (document) | Typical fix |
|---|---|
actor-isolated-call.md |
Isolate the caller to the actor, or Task { @MainActor in … } |
mutable-global-variable.md |
Make it let of a Sendable type, or isolate it with @MainActor |
sendable-closure-captures.md |
Capture by value ([x]), isolate the captured type, or as a last resort nonisolated(unsafe) |
explicit-sendable-annotations.md |
Add the conformance a public type needs, or restructure the type |
conformance-isolation.md, isolated-conformances.md |
Declare the conformance @MainActor when it needs main-actor state |
preconcurrency-import.md |
Stage an unmigrated dependency with @preconcurrency import |
nonisolated-nonsending-by-default.md |
Decide per function: caller's actor (the default) or @concurrent |
The :migrate variant of an upcoming-feature flag adds fix-its that preserve existing behavior while you adopt the feature.[^apple-nonisolated-nonsending]
</migration_diagnostics>
Protocol-Oriented Programming
<protocol_oriented> Core principle: "Don't start with a class, start with a protocol."[^abrahams-pop]
When to use protocols:
- Multiple types share behavior without sharing state
- Value types need to participate
- Multiple conformance needed (Swift = single inheritance for classes)
- Retroactive conformance to types you don't own
When to use classes:
- Need stored property inheritance
- Need to call
superimplementations - Working with UIKit/AppKit (forced)
- Identity matters more than equality
Protocol extensions = mixins: Default implementations enable code reuse without inheritance.
Anti-pattern from OOP: Treating protocols as "interfaces" with no default implementations. This recreates OOP hierarchy problems.
Dispatch rule: a method declared only in a protocol extension is statically dispatched on the compile-time type; a method that is also a protocol requirement is dynamically dispatched to the conforming type's implementation. Declare a requirement whenever conformers are expected to override. </protocol_oriented>
Value vs Reference Types
<value_vs_reference> Apple's guidance: "Use structures by default." Apple's two stated reasons to use a class are Objective-C interoperability and needing "to control the identity of the data you're modeling"; its example identity cases are file handles, network connections, and shared hardware intermediaries.[^value-semantics]
Decision tree:
Need identity semantics (object lifetime matters)? → class
Need shared mutable state? → class, or an actor when that state crosses isolation domains
Subclassing an Objective-C framework class, or passing data to an Objective-C API? → class
Need stored property inheritance? → class
Everything else → struct
Performance: a value type whose stored properties are themselves reference-free needs no heap allocation or reference counting of its own, and small ones pass in registers, so do not choose a class on the assumption that pointer semantics are cheaper; a large value, or one holding references, pays copying or retain/release costs that the optimizer may or may not remove.
Know what a stored class reference does to a value type: copies of the struct share that object, so the field has reference semantics unless the type implements copy-on-write (the standard library's technique for its collections), and each copy retains it (the optimizer removes some retain/release pairs, but design as if it didn't). Keep value types free of inner references where the field's semantics don't call for sharing; use copy-on-write when they need a shared buffer with value semantics. </value_vs_reference>
Error Handling
<error_handling>
| Mechanism | Use When |
|---|---|
throws |
Recoverable errors with context |
Result<T, E> |
Async callbacks, deferred handling |
Optional |
Simple absence, no error details needed |
Swift 6 typed throws:
enum FileError: Error { case notFound, unreadable(any Error) }
func loadFile(_ path: String) throws(FileError) -> Data {
guard fileExists(path) else { throw .notFound }
do { return try Data(contentsOf: URL(fileURLWithPath: path)) }
catch { throw .unreadable(error) } // a typed-throws body cannot propagate `any Error` unchanged
}
// Caller: error is FileError, not 'any Error'
</error_handling>
Tooling
Swift Package Manager: Three-layer architecture (Core ← Domain ← Features). Unidirectional dependencies.
Testing: Prefer Swift Testing for new unit tests (native async, #expect macro, parameterized tests). Apple's guidance is to "continue using XCTest for any tests which use UI automation APIs like XCUIApplication or use performance testing APIs like XCTMetric as these are not supported in Swift Testing."[^wwdc24-10179]
Documentation: document every public API with DocC, using ``Symbol`` links, and state algorithmic complexity when it is not O(1).
Configuration files: See local .swiftlint.yml and .swiftformat in projects for standard configs.
Swift Scripting
Choosing Swift for a Script
<scripting_decision>
| Context | Choose | Why |
|---|---|---|
| Need Foundation, AppKit, or other Apple framework APIs | Swift | Calls platform APIs directly; no bridge or wrapper layer |
| Hot path; script invoked frequently | Compiled SPM executable, or Python/Ruby | swift script.swift compiles on every run (the driver invokes -frontend -interpret each time; observed with swift -v on Swift 6.3.2) |
| Must assume only common Unix tools are present | Bash/Python | Swift requires an installed toolchain on Linux hosts[^scripting-swift-install-linux] |
| Complex data, type safety matters | Swift | Compile-time guarantees over runtime tests |
| </scripting_decision> |
Files in this skill
3swift-programmer skill questions
How do I install the swift-programmer skill in Claude Code?
Run npx degit Pyroxin/opinionated-claude-skills/opinionated-apple-development/skills/swift-programmer ~/.claude/skills/swift-programmer. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the swift-programmer skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
How should I handle concurrency in Swift 6?
Design using isolation domains rather than threads, prioritizing structured concurrency like TaskGroups and async let. Use the compiler to prove correctness rather than suppressing diagnostics.
When should I use a custom actor instead of @MainActor?
Use @MainActor for UI state and SwiftUI models. Reach for a custom actor when state is genuinely shared by concurrent work that runs off the main actor.
What are the rules for Sendable types?
Sendable types include value types with Sendable properties, actors, and final classes with immutable Sendable properties. If a type isn't thread-safe, use an actor or @MainActor instead of forcing Sendable conformance.
How do actors handle re-entrancy?
Actors are re-entrant, meaning state can change across an await point. You should perform mutations synchronously, restore consistency before every await, and re-check assumptions after the await resumes.