Detects force unwraps, force casts, force try, and other crash-prone Swift patterns that can cause unrecoverable crashes during App Store review, violating Guideline 2.1.
Install
npx skillscat add cruisediary/apple-app-review-skills/crash-risk-audit Install via the SkillsCat registry.
Skill: Crash Risk Audit
Purpose
Detects force unwraps, force casts, force try, and other crash-prone Swift patterns that can cause unrecoverable crashes during App Store review, violating Guideline 2.1.
Apple Guideline
- Primary: 2.1 — Performance: App Completeness (Crashes)
- Related: 2.1
- Reference:
references/guidelines/2-performance.md
Real-World Rejection Cases
Case: Force unwrap crash on nil response — reviewer hit edge case with test data — rejected under 2.1
Source: Apple Developer Forums (multiple threads on crash-related rejections)
Root cause: Force unwrap (!) causes EXC_BAD_INSTRUCTION crash at review time on edge-case data — reviewers use test accounts and synthetic data that may produce nil where the developer did not expect itCase: Force cast
as!crash when server returned unexpected type — reviewer triggered during review
Source: Developer blogs
Root cause: Casting without nil check produces unrecoverable crash — reviewers will encounter this on any data type mismatch between the server and the app's model layer
Trigger
Invoke on any iOS/macOS Swift project before App Store submission to identify high-risk crash patterns.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
project_root |
path | cwd | iOS/macOS project root |
shared_context |
object | nil | Pre-collected context from appstore-full-audit Phase 1 |
Actions
Phase 1: Context Collection
Skip this phase if shared_context is provided.
Glob**/*.swift— collect all Swift source files.Glob**/*.m— collect Objective-C source files.
Phase 2: Checks
Force unwrap
Run multipleGreppatterns in**/*.swiftto catch all force-unwrap variants:[a-zA-Z0-9_]!\.[a-zA-Z]— force unwrap before property/method access (value!.property)[a-zA-Z0-9_]!— force unwrap before space (let x = value!)[a-zA-Z0-9_]!,— force unwrap in argument list (foo(value!, other))[a-zA-Z0-9_]!\)— force unwrap inside parentheses (foo(value!))[a-zA-Z0-9_]!\]— force unwrap before subscript (arr[value!])[a-zA-Z0-9_]!$— force unwrap at end of line (let x = dict["key"]!)
Each match that is not in a test target or a SwiftUI#Previewblock → 🔴 CRITICAL per occurrence. Replace withif let,guard let, or??fallback.
Force cast
Greppatternas!in**/*.swift.
Each match not in a test file → 🔴 CRITICAL. Replace withas?and appropriate nil handling.Force try
Greppatterntry!in**/*.swift.
Each match not in a test file → 🔴 CRITICAL. Replace withdo { try ... } catch { }ortry?.fatalError in non-exhaustive contexts
GreppatternfatalError\(in**/*.swift.
For each match,Readsurrounding context — if used inside aswitchfor enum exhaustiveness or inrequired init(coder:)boilerplate stubs, flag → 🟠 HIGH. AnyfatalErrorreachable at runtime on valid user input is a crash risk.UI updates on background thread
GreppatternDispatchQueue\.main\.async|DispatchQueue\.main\.syncin**/*.swift— check for their absence near UI updates.
AlsoGrepforUILabel\.text\s*=|UIImageView\.image\s*=|tableView\.reloadData|collectionView\.reloadData— if these appear outside aDispatchQueue.mainblock, flag → 🟠 HIGH. Background thread UI mutations cause runtime crashes that are difficult to reproduce but may surface during review.
Phase 3: Output
Collect all findings from Phase 2 and build the prioritised findings list below. Include file paths and line numbers. Omit tiers with no findings.
Output Format
## Crash Risk Audit — Findings
### 🔴 CRITICAL — Guaranteed rejection
- [ ] TODO: Replace force unwrap with guard let or if let — crash risk on nil value — `NetworkManager.swift:45` — Guideline 2.1
- [ ] TODO: Replace `as! UserModel` with `as? UserModel` and handle nil — force cast crash risk — `FeedViewController.swift:112` — Guideline 2.1
- [ ] TODO: Replace try! with do-catch or try? — unhandled error will crash — `DatabaseManager.swift:88` — Guideline 2.1
### 🟠 HIGH — Very likely rejection
- [ ] TODO: Wrap fatalError in required init(coder:) stub with assertionFailure or a logged fallback — `CustomView.swift:22` — Guideline 2.1
- [ ] TODO: Move UILabel.text update inside DispatchQueue.main.async — UI mutation on background thread detected — `DataViewModel.swift:67`
### 🟡 MEDIUM — Possible rejection
- [ ] TODO: Audit remaining fatalError calls to verify they cannot be reached on valid user data paths
### 🟢 LOW — Best practice
- [ ] TODO: Enable Swift strict concurrency checking (SWIFT_STRICT_CONCURRENCY = complete) to catch actor-isolation violations before submissionTools Used
Glob, Grep, Read
Constraints
- Read-only. No file edits.
- No network calls.
- Skip Phase 1 if
shared_contextis provided by orchestrating agent. - Works on Swift, Objective-C, React Native, Flutter projects.
Quick Commands
Run these in your project root to check manually:
# Count force unwraps (!)
!grep -rn " as! \|try!" . --include="*.swift" | grep -v "//\|fatalError" | wc -l
# List force casts (review each one)
!grep -rn " as! " . --include="*.swift" | grep -v "//"
# Check for force try
!grep -rn "try!" . --include="*.swift" | grep -v "//"
# Check for fatalError usage
!grep -rn "fatalError\(" . --include="*.swift" | grep -v "//"Swift Anti-Pattern Reference
examples/swift/QualityPatterns.swift
Detection Steps
Find target files
- Glob:
**/*.swift(exclude*Tests*,*Spec*,*Mock*paths)
- Glob:
Search for rejection patterns
- Grep
\w![.(]— force unwrap (value!.property,value!() — avoids!=false positives - Grep
\bas!\b— force cast - Grep
\btry!\b— force try - Grep
fatalError\|preconditionFailure— unconditional crash (flag in non-test files) - Grep
\.reloadData()\|\.reloadSections\|beginUpdates— check surrounding context for DispatchQueue.main
- Grep
Determine verdict
- Force unwrap in production Swift file → 🟠 HIGH (Guideline 2.1)
- Force cast in production Swift file → 🟠 HIGH
- UITableView/UICollectionView reload outside
DispatchQueue.main→ 🔴 CRITICAL fatalErrorreachable from normal user flow → 🟠 HIGH- No crash-risk patterns in production code → 🟢 pass
Report
- File path + line number of each occurrence
- Fix: Replace
value!withguard let value = value else { return }orif let; wrap UI updates inDispatchQueue.main.async { }