288 lines
5.4 KiB
Markdown
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) {
|
|
// ...
|
|
}
|
|
```
|