Language · Swift 6.3 · Swift 6 language mode · Structured Concurrency
Swift
Value semantics, optionals and structured concurrency.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou are writing modern Swift for a codebase that compiles clean under Swift 6 language mode with full data-race safety. "Good" here means value-typed, immutable-by-default, concurrency-safe code with no force-unwraps, exhaustive error handling, and names that read as English per the Swift API Design Guidelines.
Stack
- Swift 6.3 (swift.org toolchain; Xcode 26.6 bundles Swift 6.3.3). Compile in Swift 6 language mode — strict concurrency and data-race safety are on, not opt-in. Don't target the Swift 6.4 / Xcode 27 beta toolchain or its beta-only features (async
defer,for-inover noncopyables,anyAppleOS) in code that must build on the released toolchain. - Swift Package Manager with
swift-tools-version:6.3. Manage deps inPackage.swift; commitPackage.resolved. Build/test withswift build/swift test. - Swift Testing (
import Testing,@Test,#expect,#require) — bundled in the toolchain, default for new tests. Keep XCTest only for legacy suites and the UI/performance tests it still owns; both frameworks run side by side in one test target, so migrate file-by-file. Don't mix their APIs inside a single test: no#expect/#requirein anXCTestCase, noXCTAssert*in a@Test. - Approachable Concurrency (Swift 6.2+):
nonisolated(nonsending)async by default, main-actor default isolation for app/UI targets, isolated-conformance inference. Enable via the flags below. - swift-format (official, in the toolchain:
swift format) for formatting + lint. Optionally SwiftLint for extra diagnostics (complexity, file length) it uniquely catches. - Logging:
swift-logLoggerfor portable/server code;os.Logger(OSLog) on Apple platforms. Neverprintin library or production code. - Time/async:
Duration,ContinuousClock,Task.sleep(for:). Containers:InlineArray/Span(Swift 6.2) for fixed-size, hot-path buffers;swift-collections(OrderedDictionary,Deque,OrderedSet) andswift-algorithms(chunked,windows,uniqued) instead of hand-rolling.
Project conventions
- Layout:
Sources/<Target>/,Tests/<Target>Tests/, onePackage.swiftper package. One primary type per file; filename matches the type (OrderService.swift). - Naming (API Design Guidelines):
UpperCamelCasetypes/protocols,lowerCamelCasemembers. Methods read as phrases at the call site: mutating verbsort(), non-mutating-ed/-ingnounsorted(). Booleans read as assertions:isEmpty,hasSuffix. Protocols describing capability end in-able/-ible(Equatable); protocols describing a thing are nouns (Collection). No Hungarian/_-prefixed public names, nogetprefixes. - Imports: import the smallest module. Prefer
import struct Foundation.Datastyle for one symbol in library targets to keep boundaries tight. No wildcard re-exports unless building an umbrella module. - Access control: default to
internal; markprivate/fileprivateaggressively; onlypublic/packagewhat the module contract needs. Libraries: usepackage(Swift 5.9+) for cross-target-internal API instead of over-exposingpublic. - Formatting:
swift format --in-place --recursive Sources Tests; enforce in CI withswift format lint --strict --recursive Sources Tests. Commit a.swift-formatJSON config (2-space indent, line length 120). No hand-formatting fights with the tool. - Concurrency + safety flags in every target's
swiftSettings:
swiftSettings: [
.swiftLanguageMode(.v6),
.defaultIsolation(MainActor.self), // SE-0466: app/UI/executable targets
.strictMemorySafety(), // SE-0458: makes `unsafe` uses explicit
.enableUpcomingFeature("NonisolatedNonsendingByDefault"), // SE-0461
.enableUpcomingFeature("InferIsolatedConformances"), // SE-0470
]
Omit .defaultIsolation(MainActor.self) for concurrency-heavy library targets that should stay non-isolated. .strictMemorySafety() is the dedicated SwiftSetting (equivalently the -strict-memory-safety flag) — it is not an .enableUpcomingFeature flag; StrictMemorySafety is only the diagnostic-group name.
Value types first
- Model data with
structandenum. Reach forclassonly when you genuinely need reference identity, inheritance,deinit, or Objective-C interop — say why in a comment.final classwhen a class is unavoidable and not designed for subclassing. - Reference-observing UI/domain models: use the Observation framework
@Observable(a macro on afinal class), notObservableObject/@Published. - Default properties to
let. Usevaronly for state that provably changes. Local bindings:letunless mutated. This is a data-race and correctness lever, not a style preference. - Let the compiler synthesize
Equatable,Hashable,Codable, andSendableconformances for value types; only hand-write them when the derived behavior is wrong. AddCustomStringConvertiblefor meaningful logging, notdebugPrinthacks. - Value semantics + large payloads: rely on copy-on-write (
Array,Dictionary,Stringalready do it). Hand-roll COW with an internal storage class only when profiling shows copies hurt. - Make small perf-critical or ownership-sensitive types
~Copyableand useborrowing/consumingparameter ownership to eliminate copies; expose access through borrowing accessors rather than copying the value out.
Optionals
- Never force-unwrap
!or force-castas!ortry!in shipping code. The only tolerated!is a documented, locally-provable invariant (URL(string: "https://apple.com")!for a compile-time-constant literal) with a// safe: literalcomment — prefer designing it away. - Unwrap with
if let x/guard let x(shorthand, no= x),switch, or??. Useguard let … else { return/throw }for early exit so the happy path stays unindented. - Optional chaining
a?.b?.cover nestedif letpyramids.map/flatMapon optionals for transforms. - Don't use
?? fatalError()tricks; if a nil is truly impossible, model it out of the type. AvoidOptional<Bool>tri-state flags — use anenum.
Enums and pattern matching
- Model finite states and heterogeneous payloads with
enum+ associated values (case success(Data),case failure(APIError)); avoid struct-of-optionals "one of these is set" shapes. switchover enums without adefaultclause so the compiler forces you to handle new cases. Usedefaultonly for genuinely open matching.- Use
if case/guard casefor single-case extraction,for case .some(let x)to filter,whereclauses in cases. Bind once:case let .point(x, y). - Mark enums you don't want exhaustively matched across module boundaries as
@frozenonly when you commit to never adding cases (public ABI stability); default to non-frozen.
Errors
- Throw typed, meaningful errors: an
enum ServiceError: Errorwith associated context, notNSErroror stringly-typed errors. Conform toLocalizedErroronly where a user-facing message is needed. - Use typed throws (Swift 6.0+) where the error set is closed and callers benefit:
func load() throws(ServiceError) -> Model. Keep untypedthrowsfor boundaries that propagate heterogeneous errors; don't force typed throws where it leaks implementation detail. Result<Success, Failure>for stored/deferred outcomes and completion-style boundaries you can't yet makeasync. Bridge withResult { try … }and.get(). Preferasync/throwsoverResultfor new call flows.do { … } catch let e as ServiceError { … } catch { … }. Pattern-match error cases incatch. Nevercatch {}silently — log or rethrow. Usedeferfor synchronous cleanup that must run on every exit path.- Reserve
fatalError/precondition/assertfor programmer errors and unreachable invariants, never for recoverable runtime failures.
Protocol-oriented design and generics
- Design to protocols + protocol extensions for default implementations; compose small protocols over deep class hierarchies. Add capability via
extensionconformances, retroactively with@retroactivewhen conforming a foreign type to a foreign protocol. - Prefer generics
<T: Codable>and opaque resultssome View/some Collectionover boxed existentials. Useany Protocolonly when you truly need heterogeneous storage or dynamic dispatch, and writeanyexplicitly — existentials require theanyspelling in the Swift 6 language mode (introduced by SE-0335 in Swift 5.6). - Constrain with
whereclauses and primary associated types (some Collection<Int>,any Sequence<Element>). Use parameter packs (each T) for variadic-generic APIs instead of arity-N overloads. Avoid protocols-as-types when a generic constraint gives static dispatch and specialization. - Don't reach for
AnyView/type erasure to dodge a generic signature; erase only at real boundaries. Reach for perf attributes —@inlinable/@usableFromInline, and Swift 6.3's@specialize/@inline(always)/@export— only after profiling proves the win, and keep them out of API you don't want frozen. - Expose Swift functions/enums to C with the
@cattribute (Swift 6.3) rather than manual@_cdeclshims when you need a C-callable boundary.
Concurrency (structured, data-race-safe)
- All new async code uses
async/await. No completion-handler pyramids, noDispatchQueue.asyncnesting for new code — bridge legacy callbacks once withwithCheckedThrowingContinuationand resume exactly once. Turn callback-based streams intoAsyncStream/AsyncThrowingStream. - Protect shared mutable state with
actor(or@MainActorfor UI/main-thread state). Don't guard state with locks/DispatchQueuefor new code; the actor is the unit of isolation. Keep actor methods short; hop off withnonisolatedfor pure work. Beware actor reentrancy — state can change across anawait, so re-check invariants after suspension. - Everything crossing an isolation boundary must be
Sendable. Make value typesSendable(often automatic); mark thread-safe reference types@unchecked Sendableonly with a comment explaining the manual synchronization. PreferSendableclosures typed explicitly. - Structure concurrency:
async letfor a fixed set of parallel children;withTaskGroup/withThrowingTaskGroupfor dynamic fan-out;for try awaitoverAsyncSequence. Children are awaited/cancelled with the parent — no orphanedTask {}that outlives its scope. - Honor cancellation: check
Task.isCancelled/try Task.checkCancellation()in loops; propagate it, don't swallowCancellationError. PreferwithTaskCancellationHandlerto tear down non-Swift resources. - Under Approachable Concurrency, an
asyncfunction isnonisolated(nonsending)by default — it runs on the caller's executor and doesn't hop unless you opt into@concurrent. Use@concurrentdeliberately to move work off the current actor. - Pass context implicitly with task-local values (
@TaskLocal) instead of threading it through every signature; define custom global actors sparingly (@MainActorcovers most UI cases). - Never block a thread: no
DispatchSemaphore.wait(),Thread.sleep, or synchronous.wait()inside async code. Sleep withtry await Task.sleep(for: .seconds(1))using aClock. MainActor.assumeIsolatedonly when you can prove main-thread execution; otherwiseawait MainActor.run { … }.- Control where work runs with executors, not raw threads: pin CPU-bound task groups to a
TaskExecutor(withTaskExecutorPreference) rather than spawningThreads or hopping throughDispatchQueues.
Testing
- Write new tests with Swift Testing. Free functions or methods in a
@Suitestruct annotated@Test. Assert with#expect(x == y)(soft, keeps going) and unwrap/precondition withtry #require(optional)(hard, stops the test) — these two replace allXCTAssert*. - Errors:
#expect(throws: ServiceError.notFound) { try sut.load() }or#expect(throws: (any Error).self). Async callbacks:await confirmation { confirm in … }. Known failures: wrap inwithKnownIssue { … }. Fatal-path code: cover with exit tests (await #expect(processExitsWith: .failure) { … }). - Parameterize instead of copy-pasting:
@Test(arguments: [1, 2, 3])or zipped/cross-product argument collections; each case reports independently. - Organize with
@Suite("Name"), nest suites, use.tags(.integration),.timeLimit(.minutes(1)), and.serialized(for order-dependent suites; tests run in parallel by default). Gate with.enabled(if:)/.disabled("reason"). Useinit/deinitof the suite struct for setup/teardown — nosetUp/tearDownboilerplate. - Test behavior and edge cases (nil, empty, boundary, cancellation, error paths), not getters. Keep tests deterministic: inject clocks (
ContinuousClock/a test clock), inject dependencies via protocols, no real network orsleep. Name tests as sentences describing the expectation.
Security
- Validate and parse all external input at the boundary into typed models via
Codablewith explicitCodingKeys; never trust field presence. Decode withJSONDecoderand handleDecodingError— don'ttry!. - No secrets in source or logs. Load from environment/secure store; on Apple platforms use Keychain, not
UserDefaults. Redact tokens/PII before logging;swift-logmetadata should not carry secrets. - Keep
.strictMemorySafety()on and avoidUnsafe*Pointer,unsafeBitCast,unsafeDowncast. If unavoidable, isolate in a small reviewed function, mark the callunsafe, and document the invariant. - For TLS/HTTP use
URLSession(or a vetted client); don't disable certificate validation. Build shell/DB/URL strings from typed components, never string interpolation of untrusted input. - Use
CryptoKit/swift-cryptofor hashing/crypto — never roll your own. Compare secrets in constant time. PreferDataoverStringfor key material and zero it when done.
Standard library, strings, and Foundation
- Strings are collections of
Character(grapheme clusters), not bytes. Never index with anInt— useString.Index,firstIndex(of:),prefix/suffix, or the.unicodeScalars/.utf8views chosen deliberately. Compare user-facing text withlocalizedStandardCompare/caseInsensitiveCompare, not ad-hoclowercased(). - Wrap primitive IDs in a typed
struct OrderID: Hashable, Codable, RawRepresentable(or a phantom-typedID<Order>) rather than passing bareString/UUID, so the compiler stops you mixing an order id with a user id. - Format with
FormatStyle:value.formatted(.number),price.formatted(.currency(code: "EUR")),date.formatted(.dateTime.year().month().day()),duration.formatted(.units()). Reach forNumberFormatter/DateFormatteronly for legacy interop, and cache them if you do. - Money and time: use
Decimal(or minor-unit integers) for currency andDuration/Datefor time — neverDouble. Choose an explicitJSONDecoder.dateDecodingStrategy(usually.iso8601); never parse dates by slicing strings. - Transform with
map/compactMap/filter/reduce(into:)/first(where:); use.lazyto avoid materializing intermediates on large sequences, andswift-algorithms(chunked,windows,uniqued) over hand-rolled loops. - Integer overflow traps by default — that's correct; use
&+/addingReportingOverflowonly where wrapping is intentional and documented. Pick fixed-width types (Int32,UInt8) deliberately at serialization boundaries.
Modeling and API design
- Push validation into initializers: a
failable init?orthrowsinit that rejects bad input, so once a value exists it is guaranteed valid ("parse, don't validate"). - Prefer many small, single-responsibility types with clear ownership over one god-struct of loosely related fields. Give each a focused, testable surface.
- Default function parameters over overload families; label arguments so the call site reads as a sentence. Avoid boolean parameters that are opaque at the call site — use an
enum(show(.animated)overshow(true)). - Document
public/packageAPI with///doc comments (summary,- Parameters:,- Returns:,- Throws:); keep the surface minimal and additive to protect ABI/source stability.
Do
- Return
some/opaque or concrete types; keeppublicAPI minimal and documented with///. - Prefer immutable,
Sendablevalue types passed across concurrency boundaries. - Use
guardfor preconditions and early return to keep the happy path flat. - Exhaustively
switchenums; let the compiler flag unhandled new cases. - Model impossible states out of existence with enums and non-optional types.
- Inject dependencies through protocols for testability; construct concrete types at the composition root.
- Use
Duration/ClockAPIs and structuredTaskGroups for parallelism. - Parse-don't-validate at the boundary: make invalid values unrepresentable via failable/throwing initializers.
- Run
swift build,swift format lint --strict, andswift testbefore proposing a change.
Avoid
- Force-unwrap
!, force-castas!,try!— replace withguard let/if let/as?/typedthrows. (Only exception: a commented, literal-backed invariant.) classby default — usestruct/enum;ObservableObject/@Publishedfor models — use@Observable.- Completion-handler nesting and
DispatchQueue/OperationQueueorchestration for new code — useasync/await+ actors. Bridge legacy withwithCheckedContinuation. - Locks/semaphores for shared state — use
actor; blockingThread.sleep/semaphore.wait()in async code — useTask.sleep. - Detached
Task {}fire-and-forget with no lifecycle owner; ignoringTask.isCancelled. @unchecked Sendablewithout a synchronization comment;NSError/stringly-typed errors — use typedErrorenums.default:on domain enums that swallows future cases; struct-of-optionals modeling of mutually exclusive states.printfor logging;any Protocolwhere a generic constraint would give static dispatch and specialization.- Indexing
StringbyInt, comparing user text vialowercased(), andDoublefor money — useString.Index,localizedStandardCompare, andDecimal. - Beta-toolchain-only syntax (async
defer,for-inover noncopyables,anyAppleOS) in code that must build on the released Swift 6.3 toolchain.
When you code
- Make small, focused diffs. Touch one concern per change; don't opportunistically reformat unrelated code (let
swift formatown formatting). - After editing, run
swift build(must be warning-clean under Swift 6 mode — treat concurrency warnings as errors), thenswift format lint --strict, thenswift test. Report failures; don't paper over a data-race warning with@unchecked Sendableornonisolated(unsafe). - When adding a dependency, pin it in
Package.swift, updatePackage.resolved, and justify it over the stdlib/Foundation. - Ask before: changing a target's default isolation, introducing
@unchecked Sendable/unsafe, breakingpublic/packageAPI, adding a new dependency, or switching a value type to a reference type. Otherwise proceed and note the decision. - If a requested change would force a force-unwrap, a silent
catch {}, or a blocking wait in async code, propose the safe alternative instead. - Target the released toolchain: build against Swift 6.3 (
swift --version), and don't adopt a Swift 6.4 / Xcode 27 beta feature until it ships in a stable release.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Swift 6.3 · Swift 6 language mode · Structured Concurrency.