원클릭으로
concurrency
Swift 6 concurrency patterns. Use when working with async/await, actors, MainActor isolation, or Sendable conformance.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Swift 6 concurrency patterns. Use when working with async/await, actors, MainActor isolation, or Sendable conformance.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Creates Features for dependency injection. Use when creating features, exposing public entry points, or wiring up dependencies.
Creates Repositories that abstract data access. Use when creating repositories, transforming DTOs to domain models, or implementing local-first caching. Supports remote-only, local-only, and cached (remote + local) repositories with CachePolicy.
Creates a new feature module with minimal viable structure. Use when bootstrapping a new feature from scratch, scaffolding the Tuist module, Container, Feature entry point, DeepLinkHandler, and initial screen with placeholder Text view. Includes all unit tests, mocks, stubs, and app integration. For adding domain/data layers afterward, use /datasource, /repository, /usecase. For enhancing views, use /view, /viewmodel, /navigator.
Creates Navigator for navigation. Use when setting up navigation, adding navigation to ViewModels, or testing navigation behavior.
Creates ViewModels with state management. Use when creating ViewModels, implementing ViewState pattern, or adding state management for features. Delegates to /usecase for domain use cases and to /feature for Container/Feature wiring.
Testing patterns and conventions. Use when writing unit tests, using Swift Testing framework, or following Given/When/Then structure.
| name | concurrency |
| description | Swift 6 concurrency patterns. Use when working with async/await, actors, MainActor isolation, or Sendable conformance. |
Guide for Swift 6 concurrency patterns used in this project.
This project uses Swift 6 with special build settings:
| Setting | Value | Effect |
|---|---|---|
SWIFT_APPROACHABLE_CONCURRENCY | YES | Automatic Sendable inference |
SWIFT_DEFAULT_ACTOR_ISOLATION | MainActor | All types MainActor-isolated by default |
Exception:
ChallengeNetworkingoverridesSWIFT_DEFAULT_ACTOR_ISOLATIONtononisolatedat the target level. All networking types are nonisolated by default — nononisolatedannotations needed. See the Networking README.
With SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, all types are MainActor-isolated by default.
@MainActor on ViewModels, Views, or UI-related typesnonisolated// These are automatically MainActor-isolated
final class CharacterListViewModel { } // No @MainActor needed
struct CharacterListView: View { } // No @MainActor needed
With SWIFT_APPROACHABLE_CONCURRENCY = YES, the compiler automatically infers Sendable conformance:
// This struct is automatically Sendable (all properties are Sendable)
struct User: Equatable {
let id: Int
let name: String
}
// No need to write:
// struct User: Equatable, Sendable { ... }
Rules:
Sendable (it's inferred)Types that need to run off the main thread must explicitly opt out.
Actors have their own isolation domain (not MainActor):
// Actors are NOT MainActor-isolated
actor CharacterMemoryDataSource {
private var storage: [Int: CharacterDTO] = [:]
func save(_ character: CharacterDTO) {
storage[character.id] = character
}
func get(id: Int) -> CharacterDTO? {
storage[id]
}
}
// URLProtocol subclasses are called from background threads
final class URLProtocolMock: URLProtocol, @unchecked Sendable {
nonisolated(unsafe) static var requestHandler: ((URLRequest) throws -> (URLResponse, Data?))?
nonisolated override init(
request: URLRequest,
cachedResponse: CachedURLResponse?,
client: (any URLProtocolClient)?
) {
super.init(request: request, cachedResponse: cachedResponse, client: client)
}
nonisolated override class func canInit(with request: URLRequest) -> Bool { true }
nonisolated override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
nonisolated override func startLoading() { /* ... */ }
nonisolated override func stopLoading() {}
}
// XCTestCase subclasses need nonisolated for XCTest compatibility
nonisolated final class CharacterFlowUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testCharacterFlow() throws {
let app = XCUIApplication()
app.launch()
// ...
}
}
nonisolated types (pure data types)Types that are pure data with no UI concern should be nonisolated:
// Internal networking envelope — nonisolated via module default (ChallengeNetworking)
struct GraphQLResponse<T: Decodable>: Decodable {
let data: T?
let errors: [GraphQLResponseError]?
}
All members (properties, synthesized conformances) become nonisolated automatically.
Note: In
ChallengeNetworking, types are nonisolated by default (module-level override). In other modules, use thenonisolatedkeyword explicitly.
The entire Data and Domain layer uses explicit nonisolated annotations. This ensures Data layer work (network I/O, JSON decoding, mapping) runs off MainActor when combined with @concurrent:
// Contract — nonisolated protocol with @concurrent
nonisolated protocol CharacterRepositoryContract: Sendable {
@concurrent func getCharacter(identifier: Int, cachePolicy: CachePolicy) async throws(CharacterError) -> Character
}
// Implementation — nonisolated struct with @concurrent
nonisolated struct CharacterRepository: CharacterRepositoryContract {
private let remoteDataSource: CharacterRemoteDataSourceContract
private let memoryDataSource: CharacterLocalDataSourceContract
private let mapper = CharacterMapper()
private let errorMapper = CharacterErrorMapper()
@concurrent func getCharacter(identifier: Int, cachePolicy: CachePolicy) async throws(CharacterError) -> Character {
do {
let dto = try await cachePolicy.fetch(
fromRemote: { try await remoteDataSource.fetchCharacter(identifier: identifier) },
fromCache: { await memoryDataSource.getCharacter(identifier: identifier) },
saveToCache: { await memoryDataSource.saveCharacter($0) }
)
return mapper.map(dto)
} catch {
throw errorMapper.map(CharacterErrorMapperInput(error: error, identifier: identifier))
}
}
}
// Domain model — nonisolated struct
nonisolated struct Character: Equatable {
let id: Int
let name: String
}
// Domain error — nonisolated enum + nonisolated extensions
nonisolated enum CharacterError: Error, Equatable, LocalizedError {
case loadFailed(description: String = "")
case notFound(identifier: Int)
}
nonisolated extension CharacterError: CustomDebugStringConvertible { ... }
Key rules:
nonisolated on a type does NOT propagate to extensions — each extension needs its own nonisolatedSendable@concurrent for off-MainActor executionReference: SE-0461 — Async function isolation | Improving app responsiveness
@concurrent guarantees an async function runs on the generic executor (thread pool), not on any actor. Use it for CPU-intensive work like JSON decoding + network I/O.
public protocol HTTPClientContract: Sendable {
@concurrent func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
@concurrent func request(_ endpoint: Endpoint) async throws -> Data
}
public protocol GraphQLClientContract: Sendable {
@concurrent func execute<T: Decodable>(_ operation: GraphQLOperation) async throws -> T
}
When to use:
HTTPClient, GraphQLClient) — JSON decode + network I/O happen hereWhen NOT to use:
@concurrent cannot be used with actor isolation (SE-0461)Implementation notes:
@concurrent methods must be nonisolatedChallengeNetworking uses nonisolated default — helpers, types, and inits are nonisolated automatically@concurrent methods need nonisolated init (e.g., Endpoint — already nonisolated via module default)nonisolated and @concurrent are requiredThey are complementary — each solves a different problem:
| Annotation | Purpose | Without it |
|---|---|---|
nonisolated | Removes MainActor isolation from the type/method | @concurrent on a MainActor-isolated method is a compile error — contradicts "runs on MainActor" |
@concurrent | Executes on the cooperative thread pool | nonisolated async inherits the caller's executor (SE-0338) — runs on MainActor if called from MainActor |
The flow with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor:
MainActor (default)
→ nonisolated → inherits caller's executor (SE-0338)
→ nonisolated + @concurrent → runs on thread pool (SE-0461)
nonisolated is the prerequisite for @concurrent. You cannot use @concurrent without first removing the actor isolation.
Types without async methods (DTOs, Mappers, Domain Models) also need nonisolated because:
@concurrent methods (repos, datasources)init cannot be called from a @concurrent contextnonisolated, passing them between contexts requires unnecessary actor hops// Without nonisolated on CharacterMapper:
@concurrent func getCharacter(...) async throws -> Character {
let dto = try await remoteDataSource.fetchCharacter(...)
return mapper.map(dto) // mapper.map() is MainActor-isolated → compile error
}
Use @Observable (iOS 17+), not ObservableObject:
// REQUIRED - Use @Observable
@Observable
final class CharacterListViewModel {
var state: CharacterListViewState = .idle
}
// PROHIBITED - Never use ObservableObject/@Published
final class CharacterListViewModel: ObservableObject {
@Published var state: CharacterListViewState = .idle
}
Rules:
@Observable macro (stateless ViewModels with no observable state are plain final class)ObservableObject protocol conformance@Published property wrappers@State to hold @Observable instancesThe following patterns are prohibited in this project:
// PROHIBITED - Never use these patterns
DispatchQueue.main.async { ... }
DispatchQueue.global().async { ... }
completionHandler: @escaping (Result<T, Error>) -> Void
ObservableObject / @Published // Use @Observable instead
NotificationCenter for async events
Combine for new code
Always use modern Swift concurrency:
// REQUIRED - Use async/await
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// REQUIRED - Use Task for bridging
Task {
await performAsyncWork()
}
// REQUIRED - Use actors for shared mutable state
actor DataStore {
private var cache: [String: Data] = [:]
func store(_ data: Data, forKey key: String) {
cache[key] = data
}
}
| Type | Isolation | Notes |
|---|---|---|
| View | MainActor (default) | No annotation needed |
| ViewModel | MainActor (default) | No annotation needed |
| UseCase | MainActor (default) | No annotation needed |
| Container | MainActor (default) | No annotation needed |
| Repository contract | nonisolated protocol | @concurrent on methods, explicit Sendable |
| Repository impl | nonisolated struct | @concurrent on methods |
| RemoteDataSource contract | nonisolated protocol | @concurrent on methods, explicit Sendable |
| RemoteDataSource impl | nonisolated struct | @concurrent on methods |
| DTO | nonisolated struct | Pure data, Decodable + Equatable |
| Mapper | nonisolated struct | Stateless, MapperContract |
| Domain Model | nonisolated struct | Pure data with behavior |
| Domain Error | nonisolated enum | nonisolated on extensions too |
| CachePolicy | nonisolated enum | Shared across features; carries fetch behavior via sending closures |
| HTTPClient / GraphQLClient | nonisolated (module default) | @concurrent on public methods |
| Endpoint / HTTPMethod | nonisolated (module default) | Pure data types, keep explicit Sendable for cross-module use |
| GraphQLResponse | nonisolated (module default) | Pure data envelope |
| MemoryDataSource | actor | Use actor keyword |
| URLProtocol subclass | nonisolated | Framework requirement |
| XCTestCase subclass | nonisolated | Framework requirement |
Reference: SE-0306 — Actors
Swift actors are reentrant by design. When an actor-isolated function suspends at an await, other tasks can execute on the same actor before the original function resumes. This is called interleaving.
Every await inside an actor is a suspension point where actor state can change:
// DANGEROUS — reentrancy can break invariants
actor ImageDiskCache: ImageDiskCacheContract {
private let fileSystem: FileSystemContract // `: Actor`
func image(for url: URL) async -> UIImage? {
guard let data = try? await fileSystem.contents(at: fileURL) else {
return nil
}
// ⚠️ SUSPENSION POINT — another task can run here (e.g., eviction deletes the file)
guard let attributes = try? await fileSystem.fileAttributes(at: fileURL) else {
// File was deleted between the two awaits!
return nil
}
// ...
}
}
Between two await calls on the same actor, another task (e.g., eviction) can interleave and modify the actor's state or the underlying filesystem. This leads to:
If an actor's dependency is Sendable with nonisolated methods instead of an Actor, its calls execute synchronously within the caller actor's isolation — no await, no suspension, no interleaving:
// SAFE — zero suspension points, every method is an atomic critical section
protocol FileSystemContract: Sendable {
nonisolated func contents(at url: URL) throws -> Data
nonisolated func write(_ data: Data, to url: URL) throws
// ...
}
struct FileSystem: FileSystemContract {
// FileManager is not Sendable but is documented as thread-safe.
// Safe to use from any isolation domain without synchronization.
nonisolated(unsafe) private let fileManager: FileManager
nonisolated func contents(at url: URL) throws -> Data {
try Data(contentsOf: url)
}
// ...
}
actor ImageDiskCache: ImageDiskCacheContract {
private let fileSystem: FileSystemContract
func image(for url: URL) -> UIImage? { // No `async` — fully synchronous
guard let data = try? fileSystem.contents(at: fileURL) else { return nil }
// No suspension point — no other task can interleave here
guard let attributes = try? fileSystem.fileAttributes(at: fileURL) else { ... }
// ...
}
}
| Pattern | Use when | Example |
|---|---|---|
: Actor protocol | Dependency has its own mutable state to protect | MemoryDataSource, UserDefaultsDataSource |
: Sendable + nonisolated | Dependency is a stateless wrapper around a thread-safe API | FileSystem (wraps FileManager) |
nonisolated is mandatory on protocol methodsWith SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, protocol methods without nonisolated are MainActor-isolated by default. Calling them from a non-MainActor actor requires await for the MainActor hop — reintroducing suspension points.
nonisolated(unsafe) on the property does NOT bypass method isolation — it only affects property access:
actor ImageDiskCache {
nonisolated(unsafe) private let fileSystem: FileSystemContract
// ❌ fileSystem.contents(at:) is still MainActor-isolated per protocol
// ❌ Compiler error: "Call to main actor-isolated instance method in a synchronous actor-isolated context"
}
FileManager and UserDefaults are thread-safe but not Sendable. Use nonisolated(unsafe) to store them:
struct FileSystem: FileSystemContract {
// FileManager is not Sendable but is documented as thread-safe.
// Safe to use from any isolation domain without synchronization.
nonisolated(unsafe) private let fileManager: FileManager
}
final class FileSystemMock: FileSystemContract, @unchecked Sendable {
nonisolated(unsafe) var files: [URL: Data] = [:]
nonisolated(unsafe) var writeError: (any Error)?
nonisolated(unsafe) private(set) var writeCallCount = 0
@MainActor init() {}
nonisolated func write(_ data: Data, to url: URL) throws {
writeCallCount += 1
if let writeError { throw writeError }
files[url] = data
}
}
This is safe in practice because the actor serializes all calls to the mock. Tests configure the mock on MainActor (setup) and verify on MainActor (assertions) — no concurrent access.
async throws (not completion handlers)DispatchQueue usageSendable conformance (it's inferred) — exception: public nonisolated types crossing module boundaries keep explicit Sendable because inference doesn't cross modules@MainActor on ViewModels/Views (it's default)nonisolated keyword (DTOs, Mappers, Models, Errors, Repos, Remote DS)@concurrentnonisolated extensions have their own nonisolated keyword (not inherited from type)CachePolicy.fetch closures use sending modifierawait calls reviewed for reentrancy risks: Sendable + nonisolated (not : Actor)@concurrent for off-MainActor execution