Reviewing Swift code - what to flag, and what the compiler and SwiftLint already flag for you USE WHEN: you are reviewing, critiquing or auditing existing Swift code - a "code review", a "review" of a .swift file, a diff, a PR or a pull request; deciding what to comment on in Swift; avoiding false positives on Swift code DO NOT USE FOR: writing, explaining or learning Swift - use `languages/swift`; anything the compiler or a configured SwiftLint already reports (this skill lists those so yo...
Scanned 9/8/2026
Install to Claude Code
npx -y skills add claude-dev-suite/claude-dev-suite --skill swift --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Swift?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-swift-claude-dev-suite)More formats (shields.io, HTML) on the badges page.
---
name: swift-review
description: |
Reviewing Swift code - what to flag, and what the compiler and SwiftLint already flag for you
USE WHEN: you are reviewing, critiquing or auditing existing Swift code - a
"code review", a "review" of a .swift file, a diff, a PR or a pull request;
deciding what to comment on in Swift; avoiding false positives on Swift code
DO NOT USE FOR: writing, explaining or learning Swift - use `languages/swift`;
anything the compiler or a configured SwiftLint already reports (this skill
lists those so you can stay silent about them); SwiftUI layout - use the
SwiftUI material; Kotlin Multiplatform bridging - use `languages/uniffi`
allowed-tools: Read, Grep, Glob
---
# Reviewing Swift
A reviewer's leverage is what the toolchain cannot say. Swift's compiler is
strong on nullability and exhaustiveness, so what remains clusters in the two
things it does not model by default: **object graph lifetime** (ARC will not
warn you about a cycle) and **concurrency**, whose checking is a per-target
setting rather than a language guarantee.
The snippets are fragments cut down to the defect, not runnable programs.
## Already covered - do not spend review on it
| Defect | Reported by |
|---|---|
| Using an optional without unwrapping | the compiler |
| Non-exhaustive `switch` over an enum | the compiler |
| Unused variable, unreachable code | the compiler |
| Unhandled `throws` | the compiler |
| Force unwrap `!` | SwiftLint `force_unwrapping` — **opt-in, not in the default rules** |
| Force cast `as!`, force `try!` | SwiftLint `force_cast` / `force_try` (force_cast is default) |
| Line length, naming, formatting | SwiftLint defaults |
| Data race on shared mutable state | the compiler — **only under Swift 6 language mode or `-strict-concurrency=complete`** |
The last row is the one that matters most. In Swift 5 mode with minimal
concurrency checking, every concurrency finding below is unreported. Establish
the target's setting before deciding.
> **The table above dates; the tools do not.** Verify a default set with
> `mcp__documentation__fetch_docs` on technology `swift-quality` — topics `swiftlint-rules`, `swift-6-migration`.
> Those entries point at the indexes the tool authors maintain, so they follow
> the version the project actually pins rather than the one this skill was
> written against.
## The checks that earn their place
### A closure capturing `self` strongly and outliving the call
```swift
manager.onUpdate = { self.refresh() } // manager holds the closure, closure holds self
```
**When you see it**: an `@escaping` closure stored on a long-lived object, a
`Timer`, a `NotificationCenter` observer, or a Combine sink assigned to a
property — capturing `self` without a capture list.
**Ask**: does the callee outlive `self`? ARC frees nothing in a cycle, and
nothing reports it. The leak shows up as a view controller that is never
deinitialised and keeps responding to events after its screen is gone.
`[weak self]` breaks it; a non-escaping closure does not need one.
### `[weak self]` followed by a force unwrap
```swift
load { [weak self] data in
self!.render(data) // crashes exactly in the case weak was for
}
```
**When you see it**: `guard let self = self else { return }` is correct;
`self!` or `self?.x!` is not.
**Ask**: what is the behaviour when the object is gone? The capture list says
"this may be nil", and the force unwrap says "it will not be" — one of them is
wrong. A `guard` with an early return states the intent.
### An implicitly unwrapped optional read before it is set
```swift
var client: APIClient! // nil until configure() runs
func send() { client.post(msg) } // crash if the order changes
```
**When you see it**: `var x: T!` outside an `@IBOutlet`, or a dependency
assigned in a setup method rather than an initialiser.
**Ask**: is every entry point downstream of the assignment? An IUO turns a
compile-time check into a runtime trap you opted into — the same trade as
Kotlin's `lateinit`, and worth the same scrutiny.
### An unstructured `Task` that nobody cancels
```swift
Task { await refresh() } // detached from the caller's lifetime
```
**When you see it**: `Task { }` created in a view's `onAppear`, a view model
method, or a `deinit`-less object, with the handle discarded.
**Ask**: what cancels this? An unstructured task does **not** inherit
cancellation from the scope that created it, so the work continues after the
screen is dismissed. `.task { }` in SwiftUI ties it to the view's lifetime; a
stored handle cancelled in `deinit` does it manually.
### State assumed unchanged across an `await` inside an actor
```swift
actor Cache {
func refresh() async {
guard !isLoading else { return }
isLoading = true
let data = await fetch() // other calls can run HERE
entries = data // isLoading may have been reset meanwhile
isLoading = false
}
}
```
**When you see it**: a check, then an `await`, then a mutation depending on that
check — inside an `actor` or a `@MainActor` type.
**Ask**: what else can run at the suspension point? Actors are re-entrant: they
guarantee no *simultaneous* execution, not that a method runs atomically. Every
`await` is a place another call can interleave, and the compiler will not
mention it even under strict checking.
### `try?` discarding the reason
```swift
let user = try? decoder.decode(User.self, from: data) // nil, and no idea why
```
**When you see it**: `try?` on a decode, a file read, or a network call whose
failure is actionable.
**Ask**: does the caller need to know why? `try?` collapses every distinct error
into `nil`, which then usually flows into an `else` branch reporting something
generic. `do/catch` costs three lines and keeps the diagnosis.
### A `struct` that is not actually a value type
```swift
struct Config {
let store: Storage // a CLASS: copies share this instance
}
```
**When you see it**: a `struct` with a property of class type, especially one
passed between threads or captured in a closure.
**Ask**: what does copying this actually copy? Value semantics stop at the
reference — two "copies" mutate the same `Storage`. This is also the shape that
makes a `Sendable` conformance unsound.
### An array subscript on an index that may not exist
```swift
let first = items[0] // traps on empty; there is no optional subscript
```
**When you see it**: indexing by a computed value, a count-derived index, or a
result from another collection.
**Ask**: is the index proven in range? Unlike `Dictionary`, `Array`'s subscript
does not return an optional — it traps, and a trap is not catchable. `first`,
`last` and `indices.contains` are the checked forms.
### UI work off the main actor
```swift
URLSession.shared.dataTask(with: url) { data, _, _ in
self.label.text = "done" // completion runs on a background queue
}
```
**When you see it**: a UIKit or AppKit mutation inside a URLSession completion,
a `DispatchQueue.global` block, or any non-`@MainActor` context.
**Ask**: which actor or queue is this on? Under Swift 6 the compiler catches it;
under Swift 5 with minimal checking it does not, and the failure is an
intermittent crash or a silent no-op rather than a clear error.
## Config-dependent - read the target settings before commenting
| What to read | Why it changes the review |
|---|---|
| Swift language mode (5 vs 6) | Under 6, the data-race findings are compile errors and not review findings. Under 5 they are entirely yours |
| `SWIFT_STRICT_CONCURRENCY` (`minimal`/`targeted`/`complete`) | The gradient between those two states; `targeted` checks only code that already adopted concurrency |
| `.swiftlint.yml` — is `force_unwrapping` enabled? | It is **not** a default rule, so in most projects `!` is unreported |
| Deployment target | `async`/`await` need iOS 13+ with back-deployment, Observation needs 17, Swift Testing needs 16 — do not suggest below the floor |
| Is this a package or an app target? | `Sendable` diagnostics differ, and a library's public API carries concurrency annotations into its consumers |
## What to say
Anchor the comment to the line, name the condition that triggers the defect, and
say what breaks: "`manager` holds this closure and the closure captures `self`
strongly, so the controller is never deinitialised and keeps handling events
after dismissal" beats "use `[weak self]`". If you cannot state the input that
fails, it is a preference, not a defect.
Say once whether the target uses Swift 6 mode. Half of these findings depend on
it, and repeating the caveat per comment is noise.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!