Ios concurrency
9 expert-level iOS development skills for Claude Code, Codex, and AI coding agents. SwiftUI, architecture, networking, data, security, concurrency, testing, accessibility, performance.
npx -y skills add koshkinvv/ios-agent-skills --skill ios-concurrencyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Swift Concurrency expert skill covering async/await, structured concurrency (Task, TaskGroup), actors and @MainActor, Sendable protocol and checking, AsyncSequence/AsyncStream, continuations for bridging callback APIs, Swift 6 strict concurrency mode, and common concurrency patterns (debouncing, throttling, actor-based shared state, background processing). Use this skill whenever the user writes concurrent Swift code, works with async/await, actors, or Sendable, migrates to Swift 6 concurrency, or needs to handle background work and thread safety. Triggers on: async, await, Task, TaskGroup, actor, @MainActor, Sendable, @Sendable, concurrency, AsyncSequence, AsyncStream, continuation, withCheckedContinuation, nonisolated, GlobalActor, Swift 6, strict concurrency, data race, thread safety, background task, parallel, concurrent, dispatch queue migration, or any Swift concurrency question.
SKILL.md
10.5 KB, as published. Nobody here has run it
iOS Concurrency Skill
Core Rules
- Use async/await for ALL new asynchronous code. Do not use completion handlers or Combine for async operations in new code.
- Use structured concurrency (Task, TaskGroup). Avoid unstructured task leaks. Prefer
async letorTaskGroupover spawning looseTask {}blocks. - Mark UI-updating code with @MainActor. SwiftUI views are already
@MainActor-isolated. UIKit code that touches UI must run on@MainActor. - Use actors for shared mutable state instead of locks, semaphores, or serial dispatch queues.
- All types crossing actor boundaries must be Sendable. The compiler enforces this in strict concurrency mode.
- Prefer value types (struct, enum) for Sendable. They are implicitly Sendable when all stored properties are Sendable.
- Use
withCheckedContinuationto bridge callback-based APIs to async/await. Never resume a continuation more than once. - Use
AsyncStreamfor bridging delegate/callback patterns toAsyncSequence. Task.detachedis rarely needed. UseTask {}with explicit actor isolation instead. Detached tasks lose actor context and priority inheritance.- Always handle Task cancellation cooperatively. Check
Task.isCancelledor calltry Task.checkCancellation()at appropriate points.
Decision Guide
| Scenario | Solution |
|---|---|
| Single async operation | async func / Task {} |
| Multiple independent operations | TaskGroup / async let |
| Sequential dependent operations | await one after another |
| Shared mutable state | actor |
| UI updates from background | @MainActor / MainActor.run {} |
| Bridge callback API | withCheckedContinuation / withCheckedThrowingContinuation |
| Bridge delegate pattern | AsyncStream with continuation |
| Streaming data | AsyncSequence / AsyncStream |
| Debounce user input | Task cancellation pattern |
| Non-Sendable legacy type | @preconcurrency import / @unchecked Sendable (last resort) |
| Combine publisher to async | .values property on publisher |
| Timer / periodic work | AsyncTimerSequence (from swift-async-algorithms) or AsyncStream |
| Parallel with limit | TaskGroup with semaphore-like counter |
Quick Reference: async/await
// Basic async function
func fetchUser(id: String) async throws -> User {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}
// Calling from SwiftUI
.task {
do {
user = try await fetchUser(id: "123")
} catch {
errorMessage = error.localizedDescription
}
}
// Parallel execution with async let
async let profile = fetchProfile(id: userId)
async let posts = fetchPosts(userId: userId)
async let followers = fetchFollowers(userId: userId)
let result = try await (profile, posts, followers)
// TaskGroup for dynamic parallelism
func fetchAllUsers(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await self.fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
Quick Reference: Actors
// Actor for shared state
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? {
cache[url]
}
func store(_ image: UIImage, for url: URL) {
cache[url] = image
}
}
// Usage — await is required to cross isolation boundary
let cache = ImageCache()
await cache.store(image, for: url)
let cached = await cache.image(for: url)
// @MainActor for UI
@MainActor
final class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
items = try await api.fetchItems()
} catch {
// handle error
}
}
}
Quick Reference: Sendable
// Value types — automatically Sendable if all members are
struct UserDTO: Sendable {
let id: String
let name: String
}
// Reference types — must be final with immutable stored properties
final class Configuration: Sendable {
let apiKey: String
let baseURL: URL
init(apiKey: String, baseURL: URL) {
self.apiKey = apiKey
self.baseURL = baseURL
}
}
// @unchecked Sendable — escape hatch (use with caution)
final class LegacyManager: @unchecked Sendable {
private let lock = NSLock()
private var _state: State = .idle
var state: State {
lock.withLock { _state }
}
}
Quick Reference: Continuations
// Bridge completion handler to async/await
func fetchData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacyAPI.fetch { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// Bridge delegate to AsyncStream
func locationUpdates() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
let delegate = LocationDelegate { location in
continuation.yield(location)
}
continuation.onTermination = { _ in
delegate.stop()
}
delegate.start()
}
}
Quick Reference: Task Cancellation
// Cooperative cancellation
func processItems(_ items: [Item]) async throws {
for item in items {
try Task.checkCancellation() // throws CancellationError
await process(item)
}
}
// Manual check
func fetchWithFallback() async -> Data {
if Task.isCancelled { return Data() }
// ... continue work
}
// Debounce pattern
@MainActor
final class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [Result] = []
private var searchTask: Task<Void, Never>?
func search() {
searchTask?.cancel()
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
results = await api.search(query)
}
}
}
Swift 6 Strict Concurrency Quick Guide
// Enable in Package.swift
.target(
name: "MyTarget",
swiftSettings: [.swiftLanguageMode(.v6)]
)
// Or in Xcode: Build Settings → Swift Language Version → 6
// Common fixes:
// 1. Non-Sendable type crossing isolation boundary
// → Make type Sendable or use @unchecked Sendable
// 2. Mutable capture in @Sendable closure
// → Use actor or move state inside Task
// 3. Global variable not concurrency-safe
// → Use actor, nonisolated(unsafe), or make it let
// 4. Legacy framework types not Sendable
// → @preconcurrency import FrameworkName
Anti-Patterns to Avoid
// BAD: Using Task.detached without good reason
Task.detached {
await self.doWork() // loses actor isolation and priority
}
// GOOD: Use Task {} — inherits actor context
Task {
await doWork()
}
// BAD: Blocking an actor with synchronous work
actor DataProcessor {
func process(_ data: Data) -> Result {
heavySyncComputation(data) // blocks the actor's executor
}
}
// GOOD: Move heavy sync work off the actor
actor DataProcessor {
func process(_ data: Data) async -> Result {
await Task.detached(priority: .utility) {
heavySyncComputation(data) // runs on cooperative pool
}.value
}
}
// BAD: Ignoring cancellation
Task {
for item in hugeList {
await process(item) // never checks cancellation
}
}
// GOOD: Cooperative cancellation
Task {
for item in hugeList {
try Task.checkCancellation()
await process(item)
}
}
// BAD: Resuming continuation multiple times (CRASH)
withCheckedContinuation { continuation in
api.fetch { data in
continuation.resume(returning: data)
}
api.fetch { data in // second resume — CRASH
continuation.resume(returning: data)
}
}
// BAD: Never resuming continuation (LEAK — task hangs forever)
withCheckedContinuation { continuation in
api.fetch { data in
if let data = data {
continuation.resume(returning: data)
}
// if data is nil, continuation is never resumed!
}
}
Performance Considerations
- Task creation overhead: ~1-2 microseconds. Do not create tasks in tight loops for trivial work.
- Actor contention: If many tasks await the same actor, they serialize. Keep actor methods fast.
- MainActor bottleneck: Do not run heavy computation on
@MainActor. Offload to a non-isolated async function orTask.detached. - async let: Creates a child task immediately. Only use when you actually need parallelism.
- TaskGroup: Prefer over multiple
async letwhen the number of operations is dynamic. - Sendable checking: Zero runtime cost. It is compile-time only.
- When NOT to use async/await: Pure synchronous computation, simple property access, performance-critical inner loops.
Reference Files
- async-await.md — async/await, Task, TaskGroup, continuations, AsyncSequence
- actors.md — actor, @MainActor, GlobalActor, nonisolated, Sendable
- patterns.md — common patterns, Swift 6 migration, Combine vs async/await