swift

Write Swift code for iOS/macOS following best practices. Use when developing with SwiftUI, UIKit, or Swift packages. Covers type safety, concurrency, and tooling.

allowed_tools: Read, Write, Edit, Bash, Glob, Grep

$ 安裝

git clone https://github.com/dralgorhythm/claude-agentic-framework /tmp/claude-agentic-framework && cp -r /tmp/claude-agentic-framework/.claude/skills/languages/swift ~/.claude/skills/claude-agentic-framework

// tip: Run this command in your terminal to install the skill


name: swift description: Write Swift code for iOS/macOS following best practices. Use when developing with SwiftUI, UIKit, or Swift packages. Covers type safety, concurrency, and tooling. allowed-tools: Read, Write, Edit, Bash, Glob, Grep

Swift / iOS Development

Project Setup

Swift Package Manager

# Initialize new package
swift package init --type executable

# Add dependencies to Package.swift
# swift package update

Package.swift

// swift-tools-version: 5.10
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v17), .macOS(.v14)],
    products: [
        .library(name: "MyApp", targets: ["MyApp"]),
    ],
    dependencies: [
        // Add dependencies here
    ],
    targets: [
        .target(name: "MyApp", dependencies: []),
        .testTarget(name: "MyAppTests", dependencies: ["MyApp"]),
    ]
)

Xcode Project

# Create new Xcode project via Xcode or:
# Use SwiftUI App template for new projects
# Target iOS 17+ for latest APIs

Type Patterns

Optionals

// Safe unwrapping
if let user = optionalUser {
    print(user.name)
}

// Guard for early exit
guard let user = optionalUser else {
    return
}

// Optional chaining
let name = user?.profile?.displayName ?? "Anonymous"

// Nil coalescing
let displayName = user?.name ?? "Guest"

Result Type

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingFailed
}

func fetchUser(id: String) async -> Result<User, NetworkError> {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        return .failure(.invalidURL)
    }

    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        let user = try JSONDecoder().decode(User.self, from: data)
        return .success(user)
    } catch {
        return .failure(.decodingFailed)
    }
}

// Usage
switch await fetchUser(id: "123") {
case .success(let user):
    print("Got user: \(user.name)")
case .failure(let error):
    print("Error: \(error)")
}

Protocols & Extensions

protocol Identifiable {
    var id: UUID { get }
}

protocol Displayable {
    var displayName: String { get }
}

extension User: Identifiable, Displayable {
    var displayName: String {
        "\(firstName) \(lastName)"
    }
}

Error Handling

// Throwing functions
func loadConfig() throws -> Config {
    guard let data = FileManager.default.contents(atPath: configPath) else {
        throw ConfigError.fileNotFound
    }
    return try JSONDecoder().decode(Config.self, from: data)
}

// Do-catch
do {
    let config = try loadConfig()
    print(config)
} catch ConfigError.fileNotFound {
    print("Config file missing")
} catch {
    print("Unknown error: \(error)")
}

// Try? for optional result
let config = try? loadConfig()

// Try! only when failure is impossible
let bundledConfig = try! loadBundledConfig()

Async/Await Patterns

Basic Async

func fetchUsers() async throws -> [User] {
    let (data, _) = try await URLSession.shared.data(from: usersURL)
    return try JSONDecoder().decode([User].self, from: data)
}

// Calling async functions
Task {
    do {
        let users = try await fetchUsers()
        await MainActor.run {
            self.users = users
        }
    } catch {
        print("Failed: \(error)")
    }
}

Structured Concurrency

// Parallel execution
async let users = fetchUsers()
async let posts = fetchPosts()
let (userList, postList) = try await (users, posts)

// Task groups
func fetchAllUserData(ids: [String]) async throws -> [UserData] {
    try await withThrowingTaskGroup(of: UserData.self) { group in
        for id in ids {
            group.addTask {
                try await fetchUserData(id: id)
            }
        }
        return try await group.reduce(into: []) { $0.append($1) }
    }
}

Actors

actor UserCache {
    private var cache: [String: User] = [:]

    func get(_ id: String) -> User? {
        cache[id]
    }

    func set(_ user: User) {
        cache[user.id] = user
    }
}

// Usage
let cache = UserCache()
await cache.set(user)
let cached = await cache.get("123")

SwiftUI Patterns

View with State

struct ContentView: View {
    @State private var count = 0
    @State private var username = ""

    var body: some View {
        VStack(spacing: 16) {
            Text("Count: \(count)")
                .font(.title)

            Button("Increment") {
                count += 1
            }

            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
        }
        .padding()
    }
}

Observable ViewModel

@Observable
class UserViewModel {
    var users: [User] = []
    var isLoading = false
    var error: Error?

    func loadUsers() async {
        isLoading = true
        defer { isLoading = false }

        do {
            users = try await userService.fetchAll()
        } catch {
            self.error = error
        }
    }
}

struct UserListView: View {
    @State private var viewModel = UserViewModel()

    var body: some View {
        List(viewModel.users) { user in
            UserRow(user: user)
        }
        .task {
            await viewModel.loadUsers()
        }
        .overlay {
            if viewModel.isLoading {
                ProgressView()
            }
        }
    }
}

Environment & Dependency Injection

// Define environment key
struct UserServiceKey: EnvironmentKey {
    static let defaultValue: UserService = .live
}

extension EnvironmentValues {
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }
}

// Use in view
struct ProfileView: View {
    @Environment(\.userService) private var userService

    var body: some View {
        // Use userService
    }
}

Testing

Swift Testing (Swift 6+)

import Testing

@Suite("UserService Tests")
struct UserServiceTests {
    let service = UserService()

    @Test("creates user with valid email")
    func createUser() async throws {
        let user = try await service.create(email: "test@example.com")
        #expect(user.email == "test@example.com")
    }

    @Test("throws on invalid email")
    func invalidEmail() async {
        await #expect(throws: ValidationError.self) {
            try await service.create(email: "invalid")
        }
    }

    @Test("fetches user by ID", arguments: ["user1", "user2", "user3"])
    func fetchUser(id: String) async throws {
        let user = try await service.fetch(id: id)
        #expect(user.id == id)
    }
}

XCTest (Legacy)

import XCTest
@testable import MyApp

final class UserServiceTests: XCTestCase {
    var service: UserService!

    override func setUp() {
        super.setUp()
        service = UserService()
    }

    func testCreateUser() async throws {
        let user = try await service.create(email: "test@example.com")
        XCTAssertEqual(user.email, "test@example.com")
    }

    func testInvalidEmailThrows() async {
        do {
            _ = try await service.create(email: "invalid")
            XCTFail("Expected error")
        } catch {
            XCTAssertTrue(error is ValidationError)
        }
    }
}

Tooling

# SwiftLint (linting)
brew install swiftlint
swiftlint lint
swiftlint lint --fix

# SwiftFormat (formatting)
brew install swiftformat
swiftformat .
swiftformat . --lint

# Run tests
swift test
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'

# Build
swift build
xcodebuild build -scheme MyApp

# fastlane (CI/CD)
brew install fastlane
fastlane init
fastlane ios test
fastlane ios beta  # Deploy to TestFlight

.swiftlint.yml

disabled_rules:
  - trailing_whitespace
  - line_length

opt_in_rules:
  - empty_count
  - empty_string

excluded:
  - Pods
  - .build

.swiftformat

--indent 4
--allman false
--wraparguments before-first
--wrapparameters before-first
--self remove
--importgrouping alphabetized