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

5.4 KiB

name description globs
Modern Swift Modern Swift language patterns, concurrency, and API usage
**/*.swift

Modern Swift Patterns

Use modern Swift language features and avoid deprecated patterns.

Concurrency

Always Mark Observable Classes with @MainActor

@Observable
@MainActor
final class FeatureStore {
    // All properties and methods run on main actor
}

Use Modern Concurrency (No GCD)

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// BAD
let message = "Hello, " + user.name + "!"

// GOOD
let message = "Hello, \(user.name)!"

Type Safety

Avoid Force Unwraps

// 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

// BAD
let view = cell as! CustomCell

// GOOD
guard let view = cell as? CustomCell else {
    assertionFailure("Expected CustomCell")
    return
}

Avoid Force Try

// 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

// BAD - Struct instances
.clipShape(RoundedRectangle(cornerRadius: 8))

// GOOD - Static member lookup
.clipShape(.rect(cornerRadius: 8))

Result Builders Over Imperative Construction

// 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

// 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

// 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

// BAD
let hasActive = !items.filter { $0.isActive }.isEmpty

// GOOD
let hasActive = items.contains { $0.isActive }

Use Lazy for Chained Operations

// 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 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)

func fetch(completion: @escaping (Result<Data, NetworkError>) -> Void) {
    // ...
}