How do you design a public Swift type: struct vs class vs actor vs enum?
Predict first
Your library changes public struct Config to public class Config. What happens to this client code?
var a = Config() // timeout = 10
var b = a
b.timeout = 30
print(a.timeout)
- Fails to compile
- Still prints 10
- Compiles, now prints 30
Mental model. Pick the ownership story first, then the keyword. The keyword is the easy part; the ownership story is the API.
Start with ownership. If copies should be independent → struct or enum. If there is one world object → class. If mutation must be serialized → actor, or a struct with a single isolated owner. If the set of states is closed → enum.
Then freeze that choice in the public API: flipping struct → class later is a source break, and if the type is in a resilient library it can also be an ABI break. Document isolation and sendability on the type, not in a wiki.
Also decide what you are exposing: a concrete type, a protocol, an opaque some P, or an existential any P. Prefer the narrowest surface that lets clients do their job.
No compiler error warns the client: the same code silently changes meaning. That is why the struct/class choice is part of the public contract and has to be made up front.
Common traps
- Designing for imagined future subclassing. Open-by-default classes cost you devirtualization and force you to support overrides forever.
- Leaking the implementation type through
some. Opaque types are great, but you cannot change the underlying type without a source break if clients rely on properties you expose.
They might follow up with
- What if you need to add a stored property later? If the type is resilient (non-
@frozen) that can be ABI-compatible. If it is@frozen, it is not. - How do you deprecate a type?
@available(*, deprecated, message:)plus a replacement, and never silently change layout or semantics in a patch release.
Related questions
- 1.1.4 When would you still use a class in a Swift-first codebase?
- 1.3.8 How do you evolve a public protocol without breaking clients?
- 1.10.4 What is ABI stability vs source compatibility vs module stability?
- 1.10.6 You need to add a field to a public struct in a binary SPM library used by five apps. What do you consider?
← When would you still use a class in a Swift-first codebase? When is teaching “always use structs” harmful on a team? →