Codable is Encodable & Decodable: conform a type and the compiler generates the code that maps its stored properties to and from JSON (or another format) by name. When names differ, you rename keys with a CodingKeys enum or set a key decoding strategy.
**CodingKeys**: a nested String enum whose raw values are the JSON keys. A property you leave out is skipped, so it needs a default value.
**keyDecodingStrategy = .convertFromSnakeCase**: converts every incoming key, so author_name matches authorName without any CodingKeys. Do not combine it with snake_case raw values: keys are converted first, so "author_name" would never match.
When synthesis is not enough: a non-optional property throws keyNotFound or valueNotFound for a missing or null key (make it Optional, or write init(from:)); a custom init(from:) handles a field that is sometimes Int and sometimes String.
iOSMiddle
Swift Generics & Advanced
Generics, opaque types, property wrappers, result builders, key paths
Questions closed
0 / 24
- 0Review
- 6Asked often
Questions
Generics let you write one function or type that works with any type meeting some constraints, while the compiler still checks every use: no Any, no casts. A where clause adds constraints on associated types and relations between type parameters.
Generic parameters: func clamp<T: Comparable> or struct Stack<Element>. The concrete type is fixed at each use and usually inferred from the arguments.
Constraints in angle brackets: conformances and superclasses of the parameter itself: <T: Comparable>, <T: Codable & Sendable>, and for primary associated types <C: Collection<Int>>. A where clause is still needed for relations between parameters.
**where on associated types**: where C.Element: Equatable constrains a type that belongs to the parameter.
Same-type requirements: where A.Element == B.Element ties two parameters together.
**where on an extension**: extension Stack where Element: Equatable adds methods only for some element types; extension Array: P where Element: P is conditional conformance.
Compared with Any or a protocol-typed parameter, the type survives: clamp(15, to: 0...10) returns an Int, not something you cast back. And in Release builds the optimiser can specialise the code for each concrete type.
An associated type is a placeholder type inside a protocol that each conforming type fills in, like Element in Collection. It makes the protocol generic, so the protocol name alone no longer describes one complete type.
Usually inferred: a conformer rarely writes typealias Item = User; the compiler reads it from the method signatures.
Constraints: associatedtype Item: Identifiable, plus where clauses at the use site: where S.Item == User.
The limit: before Swift 5.7 such a protocol could only be a generic constraint. any Store is allowed now, but through it you cannot call methods that take Item as a parameter, and returned Item values are erased to any Identifiable: the compiler does not know which Item the box holds.
Primary associated types: protocol Store<Item> lets you write some Store<User> and any Store<User>. With Item pinned at the use site, all methods work again, and most hand-written type erasers become unnecessary.
Choice: use a generic <S: Store> when one concrete store is used at a time, and any Store<User> when different conformers must sit in one array or property.
The third print. Box is Equatable only where Value: Equatable, and Token is not, so Box<Token> has no ==: "operator function '==' requires that 'Token' conform to 'Equatable'". The first two compile because Box<Int> and Box<String> meet the condition.
Conditional conformance: extension Type: Protocol where … makes a generic type conform only for some type arguments.
Why the array line works: Array is itself Equatable exactly when its elements are, so [Box<String>] compares. The standard library does the same for Optional, Dictionary and Set with Equatable, Hashable, Codable and Sendable, which is why [[Int]] compares and [User] encodes.
Synthesis still works: an empty extension Box: Equatable where Value: Equatable {} is enough; Hashable and Codable behave the same.
The fix: declare struct Token: Equatable, and all three lines compile.
The limit: a type conforms to a protocol in only one way, so you cannot add a second conformance with a different condition.
some Shape is an opaque type: one specific concrete type that the compiler knows and hides from the reader. any Shape is an existential: a box that can hold a value of any conforming type, and a different one each time.
**some in a return**: the function picks the type, and it must be the same type on every path. Callers see only the protocol, but the compiler keeps the real type, which is how some View hides SwiftUI's huge nested generic types.
**some in a parameter**: shorthand for a generic parameter: func f(_ s: some Shape) means func f<S: Shape>(_ s: S). The caller picks the type.
**any**: for storage where types really differ: [any Shape]. It costs a box and dynamic dispatch; the details are in the question on how generics run.
Opening: since Swift 5.7 you can pass an any Shape to a some Shape or generic parameter; the compiler opens the box for the call.
Spelling: any is required for protocols with associated types or Self requirements. For other protocols the bare name still compiles; the ExistentialAny upcoming feature makes any mandatory everywhere.
Default to some and generics; use any when different types must live in one place.
some Shape promises one concrete type for the whole function, but the two returns produce Circle and Square. The compiler says the return statements "do not have matching underlying types".
The caller never learns the type, but the compiler must still know it, and it cannot depend on a run-time value like rounded.
Fixes
**Return any Shape: each call may return a different type inside a box. Simplest when the caller only needs the protocol's API.
Wrap both in one concrete type**: an enum with a case per shape that conforms to Shape itself. The function can keep some Shape, with no box and static dispatch.
Let the caller choose: make the function generic, or split it into two functions.
The SwiftUI case
SwiftUI's var body: some View can hold an if/else of different views only because @ViewBuilder folds the branches into one _ConditionalContent type.
@escaping says the closure may be called after the function returns, because it is stored or handed to asynchronous work. Non-escaping is the default because a closure that cannot outlive the call is cheaper and safer: it cannot create a retain cycle.
What non-escaping allows: the compiler can keep the closure's context on the stack and skip retaining captures, and inside it you can use self implicitly.
What changes when it escapes: captured objects live as long as the closure. If self stores (directly or through another object) the closure that captures self, you get a retain cycle, so use [weak self]. In classes the compiler requires an explicit self. or a capture list to make the capture visible.
**No inout captures**: an escaping closure cannot capture an inout parameter, because nothing is left to write back to when it runs.
Optional closures escape implicitly: completion: (() -> Void)? cannot be marked and is treated as escaping.
Swift 6: an escaping closure that runs on another actor or thread must be @Sendable (or passed as sending), and what it captures must be Sendable.
A key path is a compile-time-checked reference to a property, written \User.name, that you can store and pass around and later use to read, or write, that property on any instance.
The types: KeyPath<Root, Value> reads; WritableKeyPath also writes, through a var of a value type; ReferenceWritableKeyPath writes through a class reference. PartialKeyPath<Root> and AnyKeyPath erase the value type, and then you can only read it as Any.
Key paths as functions: users.map(\.name) is the same as users.map { $0.name }.
Generic helpers: one sorted(by: \.age), grouped(by:) or sum(of:) instead of a closure at every call site.
In frameworks: ForEach(items, id: \.self), @Environment(\.colorScheme), SortDescriptor(\.date) in SwiftData, and assign(to:on:) in Combine all take key paths. $model.name in SwiftUI produces a Binding through @dynamicMemberLookup with a key path subscript.
Compared with Objective-C KVC strings: the compiler checks them, so renaming a property breaks the build instead of crashing at run time. Key paths are also Hashable, so they work as dictionary keys.
A property wrapper is a type marked @propertyWrapper that owns a property's storage: for @Clamped var volume the compiler stores a hidden _volume and routes every get and set through its wrappedValue.
**wrappedValue**: the only requirement. init(wrappedValue:) makes = 50 work, and extra arguments give @Clamped(0...100).
**projectedValue**: what $volume returns. In SwiftUI, $text on a @State is a Binding.
Mutating setter: if the wrapper is a struct that keeps the value inside itself, setting the property mutates the enclosing value, so the instance must be a var, and inside the type only mutating methods can assign.
**nonmutating set**: if the wrapper writes through a reference (a class box, framework-owned storage), even a let instance can assign; @State works this way. In your own value types it is a trap: copies share one box.
Typical uses: clamping and validation, UserDefaults-backed values (@AppStorage), dependency injection. A lock inside a wrapper does not make count += 1 atomic, because get and set are two separate calls.
Limits: a wrapper officially sees only its own value; reaching the enclosing instance needs the underscored _enclosingInstance subscript. Type-wide behaviour like @Observable is therefore a macro.
finished += 1 is not one locked operation: it calls the getter (lock, read, unlock), then the setter (lock, write, unlock). Two threads can read the same value and both write it plus one, so increments are lost.
Why the wrapper cannot help: the compiler turns finished += 1 into _finished.wrappedValue = _finished.wrappedValue + 1. The wrapper sees two unrelated calls. The same goes for append on a wrapped array or dict[key] = … on a wrapped dictionary.
The fix: put the whole read-modify-write under one lock: a mutate { $0 += 1 } method reached through projectedValue. Or drop the wrapper and use Mutex<Int> (withLock { $0 += 1 }) or Atomic<Int> (wrappingAdd) from the Synchronization module (iOS 18+), or an actor when callers can await.
Why Thread Sanitizer stays silent: every memory access is under the lock, so there is no data race, only a lost update. @unchecked Sendable also switches off Swift 6's checks, so the compiler cannot help either.
How to catch it: a stress test: DispatchQueue.concurrentPerform(iterations: 100_000) calling markFinished(), then XCTAssertEqual(stats.finished, 100_000). With the bug, a run ends far below that number.
A result builder is a type marked @resultBuilder whose static build… methods the compiler calls to turn the statements of a closure into one value. SwiftUI's @ViewBuilder is one. Each kind of control flow works only if the builder implements the matching method.
How the transform maps
buildExpression(_:): converts each line (optional, but lets the builder accept other types)
buildBlock(_:): combines the statements; the only required method
buildOptional(_:): if without else
buildEither(first:) / buildEither(second:): if/else and switch
buildArray(_:): for … in
buildLimitedAvailability(_:): if #available
buildFinalResult(_:): a last transform of the finished value
A missing method gives errors like "closure containing control flow statement cannot be used with result builder".
Limits
guard, while and repeat have no builder method; a local let is allowed but adds nothing to the result.
An explicit return in the closure switches the builder off for that closure.
buildPartialBlock combines children pairwise (RegexBuilder uses it), and parameter packs let buildBlock take any number of children, which is how SwiftUI dropped its old limit of ten views per container.
Used for SwiftUI views, RegexBuilder, test DSLs and HTML or attributed-string builders.
A Sequence only promises that you can step through its elements with an iterator, possibly only once. A Collection adds indices, subscripting and repeatable, non-destructive iteration. for-in needs only Sequence.
**IteratorProtocol**: one mutating func next() -> Element? that returns nil at the end. The iteration state lives here.
**Sequence**: needs makeIterator(). With it you get map, filter, reduce, contains, prefix, zip and sorted for free.
Shortcuts: sequence(first:next:), sequence(state:next:) or AnyIterator { … } when a step function is enough.
**Collection**: needs startIndex, endIndex, index(after:) and a subscript, and promises that index access is O(1). count is still O(n) in general.
The refinements: BidirectionalCollection adds cheap last and reversed(); RandomAccessCollection adds O(1) count and index jumps. With Int indices the standard library fills in the index arithmetic for you.
Why the split matters: a sequence can be single-pass or infinite (a socket stream, a random generator), so algorithms that walk the data twice exist only on Collection.
#if is a compile-time condition: code in a false branch is not compiled into the binary at all. if #available is a run-time check of the OS version the app is running on, and @available marks a declaration so the compiler makes every caller perform that check.
**#if**: tests the build, not the device: #if DEBUG, #if os(iOS), #if targetEnvironment(simulator), #if canImport(UIKit), #if compiler(>=6.0). DEBUG exists only because Xcode's Debug configuration lists it in Active Compilation Conditions; your own flags go there too.
**if #available(iOS 18, *)**: the code is in every build, but the branch runs only on iOS 18 or later. Needed whenever the deployment target is lower than the API's version. guard #available and if #unavailable exist as well.
**@available(iOS 18, *)** on a function or type moves the check to its callers. @available(*, deprecated, message: "…") is how you deprecate your own API.
The usual mix-up: #if cannot test the OS version, and #available cannot hide code that does not compile for a platform, such as a UIKit import in a macOS target.
Objective-C is exposed to Swift through a bridging header (an umbrella header in a framework). Swift is exposed to Objective-C through the generated YourModule-Swift.h, but only declarations marked @objc on NSObject subclasses and types Objective-C can represent.
Nullability: annotations (NS_ASSUME_NONNULL_BEGIN, nullable) decide whether Swift sees T or T?; without them it sees T!.
Renaming: NS_SWIFT_NAME gives an imported API a Swift-style name.
Not visible to Objective-C: structs, enums with associated values, generics, tuples, protocol extensions.
Dispatch: Swift calls are static, vtable or witness table; Objective-C messages go through objc_msgSend. @objc dynamic is needed for KVO and swizzling.
C++: -cxx-interoperability-mode=default (Swift 5.9+) lets Swift import C++ directly.
It prints start, then map 1, map 2, map 2, 4, and the same four lines again for the second first. A lazy chain stores closures, not results, so every access re-runs them, and first runs map on the match twice: once while finding the start index, once when subscripting it.
Step by step
Building squares runs nothing. .lazy on an array gives a LazyFilterSequence<LazyMapSequence<[Int], Int>> (also a collection, because the array is) that holds the array and the two closures. That is why start comes first.
first on a collection reads startIndex, then subscripts at it (this is how the current standard library implements first). The filter's startIndex walks the base and runs map on 1 (1 is odd) and 2 (4 is even). The subscript then reads the base at that index, and the lazy map runs its closure again: the second map 2.
Nothing is cached, so the second first repeats all of it.
An eager numbers.map { … }.filter { … } would print map 1 to map 6 once, before start, and allocate two arrays.
When lazy helps and when it hurts
Good for a single pass with an early exit, such as first(where:) or prefix(3), over big or infinite sequences, and it avoids intermediate arrays.
Bad when the result is read more than once, or when the closures are expensive or have side effects. count and isEmpty on a lazy filter rescan too.
If you need the values more than once, materialise them once with Array(…).
Type erasure hides a value's concrete type behind a wrapper with a fixed type, so different types that conform to the same protocol can be stored and passed as one type. AnyPublisher, AnySequence, AnyHashable and SwiftUI's AnyView are all type erasers.
How a hand-written eraser works
A generic struct such as AnyRepository<Item> conforms to the protocol itself.
Its generic init<R: Repository>(_ base: R) where R.Item == Item captures the base's methods in closures (or holds a private box class), and each requirement forwards to them.
When to use what
Generics first: if only one concrete type is used at a time, struct Screen<R: Repository> needs no box and gets specialised.
**any Repository<Order>: with primary associated types (Swift 5.7), this covers most cases where you only needed a mixed array or a swappable property.
A named eraser still wins** when the wrapper must conform to the protocol: an existential does not conform to its own protocol, so Screen<any Repository<Order>> does not compile. Also when you hide a long concrete type in a public API (eraseToAnyPublisher()), or for protocols with Self requirements, like AnyHashable for Hashable.
An enum: when the set of implementations is closed, cases with associated values need no erasure at all.
Costs
Every call goes through a closure or a box: no inlining, no specialisation, and usually an allocation per wrapper.
AnyView also hides the view structure SwiftUI uses for diffing, so it costs identity and update performance, not just speed.
The optimiser can specialise a generic function, emitting a copy for each concrete type it sees, so calls are direct and can be inlined. When it cannot, one shared body runs and receives type metadata plus witness tables; an any value always takes that indirect path, inside a box.
Unspecialised generic code
The function gets hidden parameters: the metadata for T (size, alignment and the value witness table for copy, move and destroy) and a protocol witness table for each conformance.
Every requirement call is an indirect call through a witness table, values of unknown size are handled through buffers, and nothing inlines.
Existentials
An any Shape is a container: a three-word inline buffer (larger values go to the heap), a metadata pointer and a witness table, 40 bytes on 64-bit. A class-bound existential is 16 bytes.
Calls are always dynamic, assigning a large value may allocate, and the optimiser cannot specialise through the box.
Passing an any to a generic parameter opens the box, but the callee still runs unspecialised unless the optimiser can see the concrete type.
When specialisation is lost
Module boundaries: a client cannot specialise a body it cannot see. @inlinable (with @usableFromInline on the internals it touches) publishes the body, at the cost of making it part of your ABI. Cross-module optimisation and @_specialize are the other tools.
Debug builds: at -Onone nothing is specialised, so measure in Release with Instruments.
Use generics on hot paths; use any where different types really must share one container.
A phantom type is a generic parameter that the type never stores; it exists only so the compiler can tell otherwise identical values apart. ID<User> and ID<Order> both wrap a String, but passing one where the other is expected no longer compiles.
The bug it prevents: mixing up values with the same underlying type: a user ID passed as an order ID, metres added to feet, raw input used where validated input is required.
No run-time cost: the tag is usually an empty enum that is never instantiated, and ID<User> has exactly the layout of a String.
Gating API by state: constrained extensions give methods only to some tags: SignUpForm<Validated> has submit(), SignUpForm<Draft> has only validate(). The compiler then enforces the order of steps.
Conformances pass through: Hashable, Codable and Sendable synthesise over the stored value and ignore the tag.
Cost in code: more generic signatures and conversions at module boundaries. Use it for identifiers and units that really get confused, not for every String.
Since Swift 6 a function can declare the exact error type it throws, throws(ParseError), so catch receives a ParseError instead of any Error and can switch over it exhaustively. Plain throws is still the right default for most code.
The model: throws means throws(any Error), and a non-throwing function is effectively throws(Never). Inside a do block whose calls all throw the same type, catch infers it.
Generic propagation: func retry<T, E: Error>(_ body: () throws(E) -> T) throws(E) -> T forwards the closure's exact error. A non-throwing closure makes E == Never, so the call needs no try. This replaces most uses of rethrows.
Good fits: a closed error domain the caller is expected to handle case by case (parsing, validation, an internal layer of a module); Embedded Swift and other code that must not allocate, since an any Error box may; generic code that only passes errors through.
Bad fits: public APIs that may need new failure kinds later: the error type becomes part of the contract. Layers that call URLSession, JSONDecoder or the file system also see errors you do not own and would have to wrap.
One type only: there are no error unions; you wrap other errors in your enum.
The underlying buffer is shared between copies until someone writes; before the write, a uniqueness check decides whether the buffer has to be cloned first.
The mechanism
Array, Dictionary, Set, String and Data are structs wrapping a reference to a heap buffer (String and Data keep short values inline, without a buffer). Copying the struct copies only the reference, a retain, which is why passing an array costs the same whatever its length.
Every mutating method first asks whether this struct is the only owner. If yes, it writes in place; if not, it copies the buffer and leaves the other copy untouched.
Your own type
A final storage class holding the data, kept private inside the struct.
isKnownUniquelyReferenced(&storage) on a var stored property at the top of every mutating member, cloning when it returns false.
Copy at most once: after the first write the struct owns its buffer, and later writes happen in place.
What trips implementations up
A reference that keeps pointing at the current buffer: a cached reference to the same storage object, or an iterator holding it, makes isKnownUniquelyReferenced return false on every write, so every write copies the whole buffer.
Check the stored property itself: call isKnownUniquelyReferenced(&storage) on the var property, not on a local copy of the reference: the local copy is a second owner and the answer is always false.
Copy-on-write gives value semantics, not thread safety. Two threads mutating two copies is fine; two threads touching the same variable is still a data race.
Where it shows up in profiling
Appending in a loop is amortised O(1), but appending while another variable still holds the same array pays a full copy on the first write. A struct with a large array that is copied into a view model and then mutated pays that copy every time.
removeAll() writes into the shared Storage without the uniqueness check. After var draft = saved both values point at one object, so clearing draft clears saved too: the struct quietly has reference semantics.
Why it compiles: storage is a class reference, and storage.ids.removeAll() changes the object, not the struct's stored property. The compiler sees nothing wrong; the method would compile even without mutating. Value semantics exist only where the code calls isKnownUniquelyReferenced(&storage) before a write.
The fix: one makeUnique() that clones the storage while another value still holds it, called first in every mutating member: removeAll, append, insert, subscript setters. append and insert already did this, which is why only some operations leak.
Keep the check in one place: with many mutators, send every write through one mutating accessor, such as a private writableStorage property whose getter calls makeUnique(), so a method added later cannot skip it.
How to catch it: a unit test per mutating API: copy the value, mutate the copy, assert the original did not change. Keep the storage class nested, private and final so nothing outside the struct writes to it.
Every Swift object keeps a strong and an unowned count inline in its header, and the first weak reference moves the counts into a side table that weak references point to. deinit runs when the strong count reaches zero, but the object's memory is freed only when the unowned count does too.
Three counts
Strong: ordinary references. At zero, deinit runs and the stored properties are released.
Unowned: unowned references point at the object itself. The count carries one extra +1 on behalf of all strong references, dropped after deinit, and the allocation is freed only when the count reaches zero.
Weak: lives only in the side table. The first weak reference allocates it on the heap, the inline count field becomes a pointer to it, and weak references point at the table, not at the object.
The lifecycle
Deiniting: the strong count hits zero and deinit runs. From now on a weak load returns nil and an unowned load traps.
Deinited: if unowned references remain, the object's memory stays allocated, so an unowned read finds the dead object and stops with "Attempted to read an unowned reference but object 0x… was already destroyed" instead of reading reused memory.
Freed: when the unowned count reaches zero. The side table lives on until the last weak reference is gone, so a weak reference never points at reused memory either.
In practice
unowned skips the side table and the optional, so it is cheaper than weak, but it pins the dead object's allocation until every unowned reference is gone.
A forgotten weak reference pins only the small side table.
Suppressing Copyable removes implicit copies, so the value has exactly one owner at any moment and the compiler, not code review, proves that it is closed once and never used after that.
A noncopyable struct or enum may declare deinit, which runs when its single owner goes away. Assigning it, passing it as consuming or returning it moves it: the old binding is dead, and touching it again is the compile error "used after consume". There is no reference counting and no box.
Ownership on parameters
borrowing: read-only access for the call; ownership stays with the caller.
consuming: the callee takes ownership and the caller's binding ends. An explicit close() is a consuming method that calls the system and then discard self, so deinit does not close the descriptor again.
inout: exclusive mutable access, still owned by the caller.
consume x ends a binding's lifetime at a chosen point instead of at the end of the scope.
What you give up
Generic code must opt in: a plain <T> still means <T: Copyable>, so functions and types that should accept the descriptor need <T: ~Copyable>.
Array, Dictionary and Set still require copyable elements. Optional supports noncopyable values, and so does InlineArray from Swift 6.2.
discard self is currently allowed only when every stored property is trivial, such as integers and raw pointers; a String field is rejected.
Use it for types that model a resource (a descriptor, a lock token, a transaction), not for ordinary data.
A property wrapper changes how one stored property is read and written at run time. A macro generates code at compile time and can add members, accessors and conformances to a whole declaration. @Observable must rewrite every property of a class and add a conformance, which no wrapper can do.
Property wrapper
A run-time type around one property; it officially sees only its own value, not the enclosing instance (reaching it needs the underscored _enclosingInstance subscript), and cannot add members or conformances.
Cheap: no build plugin, no extra dependency.
Macro
Freestanding, written with #: an expression or declarations (#Preview, #expect, #Predicate).
Attached, written with @, by role: member, memberAttribute, accessor, peer, extension.
Implemented with SwiftSyntax in a separate compiler-plugin target. It can only add code, the result is type-checked like hand-written code, and Expand Macro in Xcode shows it.
Why @Observable is a macro
It turns each stored property into a computed one whose getter calls access(keyPath:) and whose setter calls withMutation(keyPath:), adds an ObservationRegistrar, skips @ObservationIgnored properties and adds the Observable conformance. That gives per-property tracking: a view updates only when a property it read changes. @Published wrappers could only fire one objectWillChange for the whole object.
Costs of macros
Build time: the plugin and SwiftSyntax compile on every clean build unless a prebuilt copy is used.
Trust: Xcode asks before running a third-party macro, so CI needs that approval too (-skipMacroValidation).
Diagnostics in generated code are harder to read.
Rule: behaviour of one property means a wrapper; boilerplate across a whole type, or checks at compile time, means a macro.