ai-docs/assets/skills/swift-modern/SKILL.md

288 lines
5.4 KiB
Markdown

---
name: Modern Swift
description: Modern Swift language patterns, concurrency, and API usage
globs: ["**/*.swift"]
---
# Modern Swift Patterns
Use modern Swift language features and avoid deprecated patterns.
## Concurrency
### Always Mark Observable Classes with @MainActor
```swift
@Observable
@MainActor
final class FeatureStore {
// All properties and methods run on main actor
}
```
### Use Modern Concurrency (No GCD)
```swift
// BAD - Old GCD patterns
DispatchQueue.main.async {
self.updateUI()
}
DispatchQueue.global().async {
let result = self.heavyWork()
DispatchQueue.main.async {
self.handle(result)
}
}
// GOOD - Modern concurrency
await MainActor.run {
updateUI()
}
Task.detached {
let result = await heavyWork()
await MainActor.run {
handle(result)
}
}
```
### Use Task.sleep(for:) Not nanoseconds
```swift
// BAD
try await Task.sleep(nanoseconds: 1_000_000_000)
// GOOD
try await Task.sleep(for: .seconds(1))
try await Task.sleep(for: .milliseconds(500))
```
### Strict Concurrency Compliance
```swift
// Ensure data crossing actor boundaries is Sendable
struct Item: Sendable {
let id: UUID
let name: String
}
// Use @unchecked Sendable only when you've manually verified thread safety
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
}
```
## Foundation APIs
### Use Modern URL APIs
```swift
// BAD - Deprecated patterns
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let file = docs.appendingPathComponent("data.json")
// GOOD - Modern APIs
let docs = URL.documentsDirectory
let file = docs.appending(path: "data.json")
```
### Use Modern Date Formatting
```swift
// BAD - DateFormatter
let formatter = DateFormatter()
formatter.dateStyle = .medium
let string = formatter.string(from: date)
// GOOD - format method
let string = date.formatted(.dateTime.month().day().year())
let relative = date.formatted(.relative(presentation: .named))
```
### Use Modern Number Formatting
```swift
// BAD - C-style or NumberFormatter
let string = String(format: "%.2f", price)
// GOOD - format method
let string = price.formatted(.currency(code: "USD"))
let percent = ratio.formatted(.percent.precision(.fractionLength(1)))
```
## String Handling
### Use localizedStandardContains for User Search
```swift
// BAD - Case-sensitive or manual lowercasing
items.filter { $0.name.lowercased().contains(query.lowercased()) }
// GOOD - Locale-aware, case/diacritic insensitive
items.filter { $0.name.localizedStandardContains(query) }
```
### String Interpolation Over Concatenation
```swift
// BAD
let message = "Hello, " + user.name + "!"
// GOOD
let message = "Hello, \(user.name)!"
```
## Type Safety
### Avoid Force Unwraps
```swift
// BAD
let value = dictionary["key"]!
let url = URL(string: urlString)!
// GOOD - Guard or optional binding
guard let value = dictionary["key"] else {
throw ValidationError.missingKey
}
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
```
### Avoid Force Casts
```swift
// BAD
let view = cell as! CustomCell
// GOOD
guard let view = cell as? CustomCell else {
assertionFailure("Expected CustomCell")
return
}
```
### Avoid Force Try
```swift
// BAD
let data = try! encoder.encode(object)
// GOOD - Handle the error
do {
let data = try encoder.encode(object)
} catch {
logger.error("Encoding failed: \(error)")
}
```
## Prefer Swift-Native Patterns
### Static Member Lookup
```swift
// BAD - Struct instances
.clipShape(RoundedRectangle(cornerRadius: 8))
// GOOD - Static member lookup
.clipShape(.rect(cornerRadius: 8))
```
### Result Builders Over Imperative Construction
```swift
// BAD - Imperative array building
var views: [AnyView] = []
if showHeader {
views.append(AnyView(HeaderView()))
}
views.append(AnyView(ContentView()))
// GOOD - ViewBuilder
@ViewBuilder
var content: some View {
if showHeader {
HeaderView()
}
ContentView()
}
```
### KeyPath Expressions
```swift
// BAD
items.map { $0.name }
items.sorted { $0.date < $1.date }
// GOOD
items.map(\.name)
items.sorted(using: KeyPathComparator(\.date))
```
## Collections
### Use First(where:) Over Filter().first
```swift
// BAD - Creates intermediate array
let item = items.filter { $0.id == targetId }.first
// GOOD - Short-circuits
let item = items.first { $0.id == targetId }
```
### Use Contains(where:) Over Filter().isEmpty
```swift
// BAD
let hasActive = !items.filter { $0.isActive }.isEmpty
// GOOD
let hasActive = items.contains { $0.isActive }
```
### Use Lazy for Chained Operations
```swift
// Process large collections efficiently
let result = largeArray
.lazy
.filter { $0.isValid }
.map { $0.transformed }
.prefix(10)
.map(Array.init) // Materialize only when needed
```
## Error Handling
### Prefer Typed Throws (Swift 6 / Xcode 16+)
Typed throws requires the **Swift 6 compiler** (shipped with Xcode 16). The feature works at any deployment target, but your project must compile with Swift 6. If your team hasn't migrated yet, continue using untyped `throws`.
```swift
// Swift 6 - Typed throws
enum NetworkError: Error {
case notFound
case unauthorized
case serverError(Int)
}
func fetch() throws(NetworkError) -> Data {
// ...
}
```
### Use Result for Async Callbacks (When Not Using async/await)
```swift
func fetch(completion: @escaping (Result<Data, NetworkError>) -> Void) {
// ...
}
```