Swift 6 strict concurrency model, async/await, actors, @MainActor, Sendable, and thread safety. Use when user asks about concurrency, async/await, actors, MainActor, thread safety, data races, or migrating from GCD to structured concurrency.
/plugin marketplace add bluewaves-creations/bluewaves-skills/plugin install swift-apple-dev@bluewaves-skillsThis skill is limited to using the following tools:
Comprehensive guide to Swift 6 strict concurrency, async/await patterns, actors, and modern thread-safe programming for iOS 26 and macOS Tahoe.
Swift 6 enables complete data-race safety by default. The compiler statically verifies that your code is free from data races.
// In Package.swift
.target(
name: "MyApp",
swiftSettings: [
.swiftLanguageMode(.v6)
]
)
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Calling async functions
func loadUserProfile() async {
do {
let user = try await fetchUser(id: 123)
print("Loaded: \(user.name)")
} catch {
print("Failed: \(error)")
}
}
struct ImageLoader {
var url: URL
var image: UIImage {
get async throws {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
}
}
// Usage
let loader = ImageLoader(url: imageURL)
let image = try await loader.image
func processLines(from url: URL) async throws {
for try await line in url.lines {
print(line)
}
}
// Custom async sequence
struct Counter: AsyncSequence {
typealias Element = Int
let limit: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
let limit: Int
mutating func next() async -> Int? {
guard current < limit else { return nil }
defer { current += 1 }
try? await Task.sleep(for: .seconds(1))
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(limit: limit)
}
}
// Usage
for await count in Counter(limit: 5) {
print(count) // Prints 0, 1, 2, 3, 4 with 1s delays
}
// Unstructured task - runs independently
let task = Task {
await performWork()
}
// Detached task - no inherited context
let detached = Task.detached {
await performBackgroundWork()
}
// Task with priority
let highPriority = Task(priority: .high) {
await performUrgentWork()
}
// Cancel a task
task.cancel()
// Check cancellation
func performWork() async throws {
for item in items {
try Task.checkCancellation() // Throws if cancelled
await process(item)
}
}
func fetchAllUsers(ids: [Int]) 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 ordering
func fetchUsersOrdered(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: (Int, User).self) { group in
for (index, id) in ids.enumerated() {
group.addTask {
(index, try await fetchUser(id: id))
}
}
var results = [(Int, User)]()
for try await result in group {
results.append(result)
}
return results.sorted { $0.0 < $1.0 }.map(\.1)
}
}
// For fire-and-forget parallel tasks
await withDiscardingTaskGroup { group in
for url in urls {
group.addTask {
await prefetchImage(from: url)
}
}
// Results are automatically discarded
}
actor BankAccount {
private var balance: Decimal = 0
func deposit(_ amount: Decimal) {
balance += amount
}
func withdraw(_ amount: Decimal) throws {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
}
func getBalance() -> Decimal {
balance
}
}
// Usage - all calls are async
let account = BankAccount()
await account.deposit(100)
let balance = await account.getBalance()
actor DataManager {
private var cache: [String: Data] = [:]
// Isolated - requires await
func store(_ data: Data, for key: String) {
cache[key] = data
}
// Nonisolated - can be called synchronously
nonisolated let identifier = UUID()
nonisolated func createKey(for name: String) -> String {
"\(identifier)-\(name)"
}
}
let manager = DataManager()
let key = manager.createKey(for: "test") // No await needed
await manager.store(data, for: key) // Requires await
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private var inProgress: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
// 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 (data, _) = try await URLSession.shared.data(from: url)
let image = UIImage(data: data)!
return image
}
inProgress[url] = task
do {
let image = try await task.value
cache[url] = image
inProgress[url] = nil
return image
} catch {
inProgress[url] = nil
throw error
}
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
@Published var error: Error?
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
items = try await fetchItems()
} catch {
self.error = error
}
}
// Nonisolated for background work
nonisolated func fetchItems() async throws -> [Item] {
let url = URL(string: "https://api.example.com/items")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Item].self, from: data)
}
}
class DataProcessor {
func processData() async -> ProcessedData {
// Runs on background
let result = await heavyComputation()
return result
}
@MainActor
func updateUI(with data: ProcessedData) {
// Runs on main thread
label.text = data.summary
}
}
func performBackgroundWork() async {
let result = await processLargeDataset()
// Jump to main actor for UI update
await MainActor.run {
updateProgressView(with: result)
}
}
// Value types are implicitly Sendable
struct Point: Sendable {
var x: Double
var y: Double
}
// Immutable classes can be Sendable
final class Configuration: Sendable {
let apiKey: String
let baseURL: URL
init(apiKey: String, baseURL: URL) {
self.apiKey = apiKey
self.baseURL = baseURL
}
}
// Actors are implicitly Sendable
actor Counter: Sendable {
var count = 0
}
// Use carefully for types with internal synchronization
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
func get(_ key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
func set(_ key: String, value: Any) {
lock.lock()
defer { lock.unlock() }
storage[key] = value
}
}
// @Sendable closures can be sent across isolation boundaries
func performAsync(_ work: @Sendable @escaping () async -> Void) {
Task {
await work()
}
}
// Capturing in Sendable closures
func processItems(_ items: [Item]) {
// items must be Sendable
Task { @Sendable in
for item in items {
await process(item)
}
}
}
// New in Swift 6.2: Default to main actor isolation
// In Package.swift or build settings:
// -default-isolation MainActor
// Or per-file:
@MainActor
extension MyView {
// All methods here are MainActor-isolated by default
}
import Observation
@Observable
class Model {
var count = 0
}
// Stream changes with async sequence
func observeModel(_ model: Model) async {
for await _ in model.observations(of: \.count) {
print("Count changed to: \(model.count)")
}
}
// Compiler can now reason about value regions
func processData() async {
var data = [1, 2, 3]
// Compiler knows data doesn't escape
await withTaskGroup(of: Void.self) { group in
for item in data {
group.addTask {
print(item)
}
}
}
// Safe to mutate after task group completes
data.append(4)
}
// WRONG - flagged as unsafe in Swift 6
DispatchQueue.main.async {
self.updateUI()
}
// CORRECT - use MainActor
await MainActor.run {
self.updateUI()
}
// Or make the enclosing context @MainActor
@MainActor
func handleResult() {
updateUI() // Already on main actor
}
// WRONG - closure isolation unclear
publisher
.sink { value in
self.items = value // Data race potential
}
.store(in: &cancellables)
// CORRECT - explicit isolation
publisher
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
Task { @MainActor in
self?.items = value
}
}
.store(in: &cancellables)
// WRONG - UIViewController not Sendable
func saveData() {
let vc = self // UIViewController
Task {
await save()
vc.showSuccess() // Error: captured non-Sendable
}
}
// CORRECT - use weak capture and MainActor
func saveData() {
Task { [weak self] in
await save()
await MainActor.run {
self?.showSuccess()
}
}
}
actor DataStore {
var items: [Item] = []
func addItem(_ item: Item) async {
// State before await
let countBefore = items.count
await saveToDatabase(item)
// WARNING: items may have changed during await!
items.append(item) // Could cause duplicates
}
// BETTER - capture state carefully
func addItemSafely(_ item: Item) async {
items.append(item)
let itemsCopy = items
await saveToDatabase(itemsCopy)
}
}
func loadData(completion: @escaping (Result<Data, Error>) -> Void) {
DispatchQueue.global().async {
do {
let data = try self.fetchDataSync()
DispatchQueue.main.async {
completion(.success(data))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
func loadData() async throws -> Data {
try await fetchData()
}
// Usage
Task {
do {
let data = try await loadData()
// Already on calling context
} catch {
// Handle error
}
}
// Wrap completion handler API
func fetchLegacyData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacyFetchData { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// Important: Only call resume ONCE
func fetchWithTimeout() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
var hasResumed = false
legacyFetch { data in
guard !hasResumed else { return }
hasResumed = true
continuation.resume(returning: data)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
guard !hasResumed else { return }
hasResumed = true
continuation.resume(throwing: TimeoutError())
}
}
}
func notifications() -> AsyncStream<Notification> {
AsyncStream { continuation in
let observer = NotificationCenter.default.addObserver(
forName: .customNotification,
object: nil,
queue: .main
) { notification in
continuation.yield(notification)
}
continuation.onTermination = { _ in
NotificationCenter.default.removeObserver(observer)
}
}
}
// Usage
for await notification in notifications() {
print("Received: \(notification)")
}
func dataStream(from url: URL) -> AsyncThrowingStream<Data, Error> {
AsyncThrowingStream { continuation in
let task = URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
continuation.finish(throwing: error)
return
}
if let data = data {
continuation.yield(data)
}
continuation.finish()
}
task.resume()
continuation.onTermination = { _ in
task.cancel()
}
}
}
import Testing
@Test("Concurrent counter increments correctly")
func concurrentCounter() async {
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask {
await counter.increment()
}
}
}
let final = await counter.value
#expect(final == 1000)
}
@Test(.serialized, "Tests that must run sequentially")
func sequentialTest() async {
// Use .serialized trait for tests not ready for parallel
}
Task.isCancelled in long operations.serialized trait when neededThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.