Install
openclaw skills install @ivangdavila/swiftWrites, debugs, and optimizes Swift — concurrency, ARC leaks, optionals, generics, SwiftUI state, and packages. Use when writing or reviewing Swift code, when a build won't type-check or the compiler "is unable to type-check this expression in reasonable time", when an app crashes on a force unwrap, an unowned reference, or EXC_BAD_ACCESS, when Swift 6 raises Sendable / actor-isolated / data-race errors, when a retain cycle keeps objects alive, when JSON fails to decode, when async code hangs or deadlocks, when SwiftUI state resets or views redraw too often, when XCTest or Swift Testing tests are flaky, when a Swift package won't resolve, or when bridging to Objective-C, C, or Linux. Not for Xcode signing and IDE settings, or App Store submission and iOS app lifecycle.
openclaw skills install @ivangdavila/swiftUser preferences and memory live in ~/Clawic/data/swift/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/swift/ or ~/clawic/swift/), move it to ~/Clawic/data/swift/.
Sendable conformance failures, and actor-isolation diagnostics during a language-mode migrationdeinit, growth Instruments blames on nothingxcode), and not for App Store review, app lifecycle, or platform frameworks (→ ios)| Situation | Play |
|---|---|
| Crash with a Swift message ("Unexpectedly found nil", "Index out of range") | Decode it in Crash Messages below, then debugging.md for the chain |
| Crash with no message (EXC_BAD_ACCESS, bare SIGTRAP) | Memory Graph, zombies, then sanitizers → debugging.md |
Object never deallocates, deinit never runs | Retain cycle: closure, delegate, timer, or observer → arc.md |
| "Sending value of non-Sendable type", "actor-isolated property" | concurrency.md for the rule, swift6-migration.md for the order to fix them in |
| Async code hangs with no error and no CPU | Continuation never resumed, or a blocked cooperative thread (rule 4) → concurrency.md |
| Upgrading a codebase to Swift 6 language mode | Per-target ratchet, not a big bang → swift6-migration.md |
Too many !, try!, or IUOs; nil handling is noisy | optionals.md |
Designing the failure channel: throws vs Result vs optional, typed throws, retry, cancellation | errors.md |
| JSON decode throws, keys missing, dates rejected | codable.md (fractional-second ISO 8601 is the usual one) |
any vs some vs generic; a protocol default never gets called | types.md |
| Unicode, string indices, substrings, regex, locale-sensitive compare | strings.md |
| Array/Dictionary/Set behavior, ordering, or an O(n²) blowup | collections.md |
| Arithmetic traps at runtime, a numeric conversion crashes, floats compare wrong, or money needs a type | numerics.md |
| SwiftUI state resets, views redraw constantly, layout fights back | swiftui.md |
| Tests flaky, async tests pass before the work finishes | testing.md |
| Package won't resolve, resources missing, plugin blocked | packages.md |
| Slow build, slow runtime, oversized binary | performance.md |
| Objective-C, C, or C++ bridging; unsafe pointers; C callbacks | interop.md |
| Builds on macOS, fails on Linux or in a container | linux.md |
| Designing public API: naming, access control, availability, ABI | api-design.md |
| Property wrappers, result builders, macros, key paths | metaprogramming.md |
| Anything else | Reduce to the smallest file that still reproduces, compile with -Onone, and read the FIRST compiler error — the rest is usually cascade noise |
! is a deliberate crash, not a shortcut. Recoverable nil → guard let x else { return }. Nil that means the program is already broken → guard let x else { fatalError("config missing: \(key)") }; the crash log then names the invariant instead of "Unexpectedly found nil while unwrapping an Optional value", which names nothing. Only IBOutlets and two-phase init justify an implicitly unwrapped T!.weak when the target can die first; unowned only when lifetimes are provably nested. Test: can the referenced object deallocate while this reference exists? Yes → weak (becomes nil, no crash). Provably no, because it owns me → unowned (one less check, but a dangling read is an immediate crash rather than a nil). Escaping closures that touch self: [weak self] in guard let self else { return }. Non-escaping closures (map, forEach, withLock) never need a capture list — they cannot outlive the call.struct. Reach for class/actor when the thing has identity (two copies with equal fields are still different things), needs deinit, or is genuinely shared and mutated in place. Copy cost is not the tiebreaker: standard-library collections are copy-on-write, so a copy is a retain until someone writes (→ types.md).DispatchSemaphore.wait() calls inside async functions deadlock the whole process — no error, no timeout. Bridge blocking work out to a dedicated DispatchQueue or thread, and await everything else.withCheckedContinuation resumes exactly once on every path. Twice = runtime trap ("SWIFT TASK CONTINUATION MISUSE"). Zero times = the awaiting task hangs forever with no error and no log. Audit every early return, every error branch, and every delegate callback that can fire more than once. Use the Checked variants everywhere except measured hot paths — the Unsafe ones diagnose nothing.awaits. Actors serialize access but are reentrant: any await inside an actor method lets other calls run and mutate state. Check-then-act across a suspension is a bug. Deduplicate in-flight work by storing the Task handle and returning it, not by flipping an isLoading flag around an await.@unchecked Sendable is a promise you must implement. Legal only when every mutable stored property is reached through one lock (Mutex, NSLock, serial queue) with no path around it, and that lock is documented in the type. Otherwise the compiler stops checking and the race ships. Prefer making the type actually Sendable: let properties of Sendable types, or an actor.-Xfrontend -warn-long-expression-type-checking=500 -Xfrontend -warn-long-function-bodies=500 (milliseconds) and split whatever it flags — long + chains, mixed numeric literals, and large collection literals without type annotations are the repeat offenders. Annotating one intermediate let routinely takes a multi-second expression under the threshold.Swift runtime traps surface as EXC_BREAKPOINT on arm64 and EXC_BAD_INSTRUCTION on x86_64; the human-readable line goes to stderr and lands in crash reports under Application Specific Information. No message at all means it is not a Swift trap — it is a memory error.
| Message | Means | First move |
|---|---|---|
| "Unexpectedly found nil while unwrapping an Optional value" | ! or IUO on nil | Symbolicate to the line, then replace with guard let or a fatalError that names the invariant (rule 1) |
| "Index out of range" | Collection subscript past the end | Off-by-one, or an index captured before a mutation; enumerated() offsets are not slice indices (collections.md) |
| "Attempted to read an unowned reference but the object was already deallocated" | unowned outlived its target | Switch to weak; the lifetimes were not nested (rule 2) |
| "SWIFT TASK CONTINUATION MISUSE: … tried to resume its continuation more than once" | Continuation resumed twice | One-shot guard around the resume, or restructure the callback (rule 5) |
| "Simultaneous accesses to 0x…, but modification requires exclusive access" | Exclusivity violation | Two inout arguments aliasing one variable, or a mutating method re-entering itself; copy to a local first |
| "Fatal error: Duplicate keys of type 'X' were found in a Dictionary" | == and hash(into:) disagree, or duplicates in uniqueKeysWithValues | Hash exactly the fields == compares (collections.md) |
| "sort predicate is not a strict weak ordering" | Comparator inconsistent, or NaN in the data | Make a<b, b<a, and equality mutually exclusive; filter NaN before sorting |
| "Range requires lowerBound <= upperBound" | Computed range inverted | Clamp before constructing — empty ranges are legal, inverted ones trap |
| "Arithmetic operation '… + 1' (on type 'Int') results in an overflow" | Signed/unsigned overflow on +, -, *, or << — traps in release too | Widen the accumulator or use addingReportingOverflow; &+ only when wrapping IS the algorithm (numerics.md) |
| "Division by zero", "Division results in an overflow" | Integer / or % with a zero divisor, or Int.min / -1 | Guard the divisor where it enters the system; floating-point division yields .infinity instead of trapping |
| "Negative value is not representable", "Double value cannot be converted to Int…" | Signed → unsigned, or a NaN/infinite/out-of-range Double → integer | Int(exactly:) for untrusted values, Int(clamping:) to saturate |
| EXC_BAD_ACCESS / KERN_INVALID_ADDRESS with no Swift message | Dangling pointer, over-release, unowned(unsafe), or a C pointer escaping its withUnsafe… scope | Zombies first, then Address Sanitizer (debugging.md, interop.md) |
| Hang, low CPU, no message | Blocked cooperative thread (rule 4) or an unresumed continuation (rule 5) | Pause in the debugger, bt all, look for tasks parked on a semaphore or lock |
Swift traps on integer overflow and on impossible conversions in release builds too — arithmetic is a crash surface, not only a correctness one. Choose the operator that states the intent:
| Intent | Write | Not |
|---|---|---|
| Ordinary arithmetic on validated values | +, -, * | &+ "to be safe": wrapping hides the bug the trap was reporting |
| Wrapping IS the algorithm (hash, checksum, PRNG, ring index) | &+, &*, &<< | +, which traps the first time real input wraps |
| Overflow is possible and recoverable | let (v, o) = a.addingReportingOverflow(b) | do/catch — arithmetic never throws, it traps |
| Convert a value that might not fit | Int(exactly: x) → nil, or Int(clamping: x) to saturate | Int(x), which traps on out-of-range, NaN, and infinity |
| Money, prices, tax, invoice lines | Integer minor units, or Decimal built from a string | Double at any stage, including "only for display" |
Compare two computed Doubles | abs(a - b) <= max(abs(a), abs(b)) * .ulpOfOne * 4 | ==, which fails for 0.1 + 0.2 |
| Midpoint or average of two integers | a + (b - a) / 2 | (a + b) / 2, which overflows near the bounds |
| Test evenness | x.isMultiple(of: 2) | x % 2 == 1, false for every negative odd number (% takes the dividend's sign) |
| Sort or reduce floats that may contain NaN | Filter on isFinite first | sorted(), which trips the strict-weak-ordering trap |
Overflow, conversion, floating-point and money depth: numerics.md.
Before emitting Swift code, verify:
!, try!, as!, or IUO beyond what force_unwrap_policy allows, and each survivor has a reason a reviewer would acceptself has a capture list; every unowned passes the nested-lifetime test (rule 2)withCheckedContinuation resumes exactly once on every path, thrown errors included.wait(), Thread.sleep, synchronous file or network I/O) inside an async function (rule 4)let, actor-isolated, or explicitly justified — it is a data-race error in language mode 6catch {}, and CancellationError rethrown rather than swallowed (errors.md)exactly:/clamping: or provably in range, and no monetary value passes through Doubledeployment_floor is set (api-design.md)User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/swift/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| swift_language_mode | 5 | 6 | 6 | Whether data-race diagnostics are errors or warnings; drives all advice in concurrency.md and whether swift6-migration.md is offered |
| build_tool | swiftpm | xcode | swiftpm | Examples use swift build/swift test or a scheme-based invocation; changes where build settings are edited (packages.md) |
| test_framework | swift-testing | xctest | both | swift-testing | Syntax of generated tests and which half of testing.md applies |
| target_platforms | apple | linux | cross-platform | apple | Gates Foundation, @objc, and Dispatch assumptions; cross-platform forces #if canImport guards (linux.md) |
| deployment_floor | text (e.g. "iOS 17, macOS 14") | none | When set, every suggested API is checked against it and wrapped in @available/#available instead of assumed present (api-design.md) |
| force_unwrap_policy | forbidden | tests-only | allowed | tests-only | Whether emitted non-test code may contain !, try!, or IUO; forbidden also rejects as! |
| money_representation | minor-units | decimal | minor-units | Which type the Arithmetic Defaults row for money resolves to when emitting a currency model |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:
metaprogramming.md)self., doc-comment expectations, one type per file, naming of async vs completion-handler variantstry!, fatalError, @unchecked Sendable, unsafe pointers, and wrapping operators, and whether to flag them unprompted or only on reviewAsyncSequence, the vetted third-party list| Trap | Why it fails | Do instead |
|---|---|---|
[weak self] on every closure | Non-escaping closures cannot outlive the call, so the capture list buys nothing and adds an unwrap on every line | Capture list only on escaping and stored closures (rule 2) |
DispatchQueue.main.async inside an async function | Hops to main by hand, defeats actor isolation, and hides the requirement from the compiler | @MainActor on the function or type; MainActor.assumeIsolated when already provably on main |
Task { } fired and forgotten | Unstructured tasks are not cancelled when their scope ends, and a thrown error inside vanishes silently | async let/TaskGroup for scoped work; store the handle and cancel it when it must stay unstructured |
@unchecked Sendable to silence the compiler | Converts a compile error into a race that only appears under load | Fix the isolation, or implement the lock the annotation promises (rule 7) |
Retry loop whose catch also catches cancellation | Swallowing CancellationError makes the task uncancellable and the loop immortal | Rethrow cancellation first, then handle domain errors (errors.md) |
print() left in shipping code | The string is fully constructed even when nothing reads it, and stdout is synchronized | os.Logger or #if DEBUG; Logger interpolation is lazy and redacts by default |
Storing a Substring | A Substring keeps its parent String's entire buffer alive — parse a 10 MB file, keep 20 short fields, keep 10 MB | String(substring) at the boundary (strings.md) |
Array(repeating: MyClass(), count: n) | The initializer runs once; all n slots hold the SAME instance | (0..<n).map { _ in MyClass() } |
Comparing optionals with < | Optional lost its comparison operators in Swift 3; code that "worked" was comparing something else | Unwrap first, or compare with an explicit default |
| Force-trying a decode to "see the error" | try! destroys the DecodingError, the only thing that names the offending key path | do/catch and print the error in full (codable.md) |
any existentials vs generics. Existential-first reads better and compiles faster; generic-first runs faster (no boxing, no witness-table lookups) and specializes. The honest boundary is measurement: any for heterogeneous storage and API surfaces, some/generics on hot paths and wherever the concrete type is already known.#expect with expression capture, in-process parallelism). XCTest still owns UI automation and performance measurement, so mixed suites are normal rather than a smell. Migrating a green XCTest suite for style alone buys nothing.More Clawic skills, get them at https://clawic.com/skills/swift (install if the user confirms):
ios — shipping iOS apps: lifecycle, frameworks, App Storexcode — signing, build settings, derived data, IDE-level failuresmacos — macOS system behavior and command-line differencesPart of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/swift.