본문으로 건너뛰기
cccskills
무료GitHub에 공개

Swift Concurrency

Modern Swift concurrency with async/await, actors, TaskGroup, Sendable, and proper actor isolation for thread-safe iOS development

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

실행 점수 미검증

설치 방법 보기

포함된 파일(2)

  • SKILL.md10.2 KB
  • references/migration-to-swift6.md3.4 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Swift Concurrency Skill

Master Swift 6 structured concurrency for safe, efficient concurrent code. Covers async/await, actors, tasks, and the Sendable protocol.

async/await Fundamentals

Basic Async Functions

// Async function declaration
func fetchUser(id: String) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw NetworkError.invalidResponse
    }

    return try JSONDecoder().decode(User.self, from: data)
}

// Calling async functions
func loadProfile() async {
    do {
        let user = try await fetchUser(id: "123")
        print("Loaded: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

Async Properties

struct RemoteImage {
    let url: URL

    var data: Data {
        get async throws {
            let (data, _) = try await URLSession.shared.data(from: url)
            return data
        }
    }
}

Async Sequences

// Creating async sequences
func fetchPages() -> AsyncThrowingStream<Page, Error> {
    AsyncThrowingStream { continuation in
        Task {
            var cursor: String? = nil
            repeat {
                let response = try await fetchPage(cursor: cursor)
                for page in response.pages {
                    continuation.yield(page)
                }
                cursor = response.nextCursor
            } while cursor != nil
            continuation.finish()
        }
    }
}

// Consuming async sequences
func processPages() async throws {
    for try await page in fetchPages() {
        process(page)
    }
}

Tasks

Unstructured Tasks

// Fire-and-forget task (inherits actor context)
func startBackgroundWork() {
    Task {
        await performWork()
    }
}

// Detached task (no inherited context)
func startDetachedWork() {
    Task.detached(priority: .background) {
        await performHeavyWork()
    }
}

Task Cancellation

func downloadWithCancellation() async throws -> Data {
    // Check for cancellation
    try Task.checkCancellation()

    // Or check manually
    if Task.isCancelled {
        throw CancellationError()
    }

    // Cooperative cancellation in loops
    for item in items {
        try Task.checkCancellation()
        await process(item)
    }

    return data
}

// Cancelling tasks
class DownloadManager {
    private var downloadTask: Task<Data, Error>?

    func startDownload() {
        downloadTask = Task {
            try await downloadLargeFile()
        }
    }

    func cancelDownload() {
        downloadTask?.cancel()
    }
}

TaskGroup for Parallel Work

// Parallel fetching with TaskGroup
func fetchAllUsers(ids: [String]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                try await fetchUser(id: id)
            }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// With result collection
func fetchWithResults(ids: [String]) async -> [Result<User, Error>] {
    await withTaskGroup(of: Result<User, Error>.self) { group in
        for id in ids {
            group.addTask {
                do {
                    return .success(try await fetchUser(id: id))
                } catch {
                    return .failure(error)
                }
            }
        }

        var results: [Result<User, Error>] = []
        for await result in group {
            results.append(result)
        }
        return results
    }
}

Actors

Basic Actor

actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) throws {
        guard balance >= amount else {
            throw BankError.insufficientFunds
        }
        balance -= amount
    }

    func getBalance() -> Double {
        balance
    }
}

// Usage (requires await)
let account = BankAccount()
await account.deposit(100)
let balance = await account.getBalance()

nonisolated Methods

actor DataStore {
    private var cache: [String: Data] = [:]

    // Requires isolation (default)
    func store(_ data: Data, for key: String) {
        cache[key] = data
    }

    // No isolation needed - can be called synchronously
    nonisolated var description: String {
        "DataStore instance"
    }

    // Computed from immutable state
    nonisolated let identifier = UUID()
}

Actor Reentrancy

actor ImageLoader {
    private var cache: [URL: Image] = [:]
    private var inProgress: [URL: Task<Image, Error>] = [:]

    func load(from url: URL) async throws -> Image {
        // Check cache first
        if let cached = cache[url] {
            return cached
        }

        // Check if already loading
        if let existing = inProgress[url] {
            return try await existing.value
        }

        // Start new load
        let task = Task {
            let image = try await downloadImage(from: url)
            cache[url] = image
            inProgress[url] = nil
            return image
        }

        inProgress[url] = task
        return try await task.value
    }
}

@MainActor

UI Updates on Main Thread

@MainActor
final class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var error: Error?

    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func loadUser(id: String) async {
        isLoading = true
        defer { isLoading = false }

        do {
            user = try await repository.fetch(id: id)
        } catch {
            self.error = error
        }
    }
}

Calling MainActor from Background

actor DataProcessor {
    func process(items: [Item]) async {
        let results = heavyProcessing(items)

        // Switch to main actor for UI update
        await MainActor.run {
            UIManager.shared.updateUI(with: results)
        }
    }
}

Sendable Protocol

Understanding Sendable

// Value types are implicitly Sendable
struct Point: Sendable {
    var x: Double
    var y: Double
}

// Classes must be explicitly marked and thread-safe
final class Counter: Sendable {
    private let lock = NSLock()
    private var _count = 0

    var count: Int {
        lock.withLock { _count }
    }

    func increment() {
        lock.withLock { _count += 1 }
    }
}

// Actors are implicitly Sendable
actor SafeCounter: Sendable {
    var count = 0

    func increment() {
        count += 1
    }
}

@unchecked Sendable

// Use when you guarantee thread safety manually
final class LegacyCache: @unchecked Sendable {
    private let queue = DispatchQueue(label: "cache.queue")
    private var storage: [String: Any] = [:]

    func get(_ key: String) -> Any? {
        queue.sync { storage[key] }
    }

    func set(_ key: String, value: Any) {
        queue.async { [self] in
            storage[key] = value
        }
    }
}

Sendable Closures

actor DataManager {
    func process(completion: @Sendable @escaping () -> Void) {
        Task {
            await doWork()
            completion()
        }
    }
}

Global Actors

Custom Global Actors

@globalActor
actor DatabaseActor: GlobalActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
final class DatabaseManager {
    private var connection: Connection?

    func query(_ sql: String) -> [Row] {
        // Runs on DatabaseActor
        connection?.execute(sql) ?? []
    }
}

// Usage
@DatabaseActor
func fetchRecords() async -> [Record] {
    let manager = DatabaseManager()
    return manager.query("SELECT * FROM records")
}

Common Patterns

Async Initialization

actor ConfiguredService {
    private var config: Config?

    func configure() async throws {
        config = try await loadConfig()
    }

    func performAction() async throws {
        guard let config else {
            throw ServiceError.notConfigured
        }
        // Use config
    }
}

// Factory pattern for async init
extension ConfiguredService {
    static func create() async throws -> ConfiguredService {
        let service = ConfiguredService()
        try await service.configure()
        return service
    }
}

Timeout Pattern

func fetchWithTimeout<T>(
    seconds: Double,
    operation: @escaping () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask {
            try await operation()
        }

        group.addTask {
            try await Task.sleep(for: .seconds(seconds))
            throw TimeoutError()
        }

        let result = try await group.next()!
        group.cancelAll()
        return result
    }
}

// Usage
let user = try await fetchWithTimeout(seconds: 10) {
    try await fetchUser(id: "123")
}

Rate Limiting

actor RateLimiter {
    private let limit: Int
    private let interval: Duration
    private var requests: [Date] = []

    init(limit: Int, per interval: Duration) {
        self.limit = limit
        self.interval = interval
    }

    func acquire() async throws {
        let now = Date()
        let cutoff = now.addingTimeInterval(-interval.timeInterval)
        requests = requests.filter { $0 > cutoff }

        if requests.count >= limit {
            let waitTime = requests.first!.timeIntervalSince(cutoff)
            try await Task.sleep(for: .seconds(waitTime))
            try await acquire()
        } else {
            requests.append(now)
        }
    }
}

References

  • See references/migration-to-swift6.md for Swift 6 strict concurrency migration
  • See references/concurrency-debugging.md for debugging race conditions

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

When the user wants to A/B test App Store product page elements to improve conversion rate. Also use when the user mentions "A/B test", "product page optimization", "test my screenshots", "test my icon", "conversion rate optimization", "CPP", or "custom product pages". For screenshot design, see screenshot-optimization. For metadata optimization, see metadata-optimization.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

JordanCoin/ios-skills-collection62026년 9월 10일 업데이트

Discover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

JordanCoin/ios-skills-collection62026년 9월 10일 업데이트

Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

JordanCoin/ios-skills-collection62026년 9월 10일 업데이트

Measure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

JordanCoin/ios-skills-collection62026년 9월 10일 업데이트

alarmkit

무료

Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, and Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, AlarmButton stop and snooze actions, authorization, state observation, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

JordanCoin/ios-skills-collection62026년 9월 10일 업데이트

When the user wants to optimize their Google Play Store listing — title, short description, full description, keywords, ratings, or Play Store-specific features. Use when the user mentions "Google Play", "Android", "Play Store", "Play Console", "short description", "full description indexed", "Google Play ASO", or wants Google Play-specific keyword, creative, or ratings strategy. For iOS App Store optimization, see aso-audit and metadata-optimization.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

JordanCoin/ios-skills-collection62026년 9월 10일 업데이트

JordanCoin의 스킬 모두 보기

이 스킬의 문제 신고