Swift `some` vs `any`
`some` and `any` are two keywords that look almost identical but represent opposite guarantees. This post explains opaque and existential types, how they're implemented, when to use each, and what they mean for performance.
The previous post on associatedtype ended with a problem: once a protocol has an associated type, you can't use it as a plain type. var views: [View] doesn't compile. The compiler doesn't know what Body is for each element.
The workarounds mentioned were some View, AnyView, @ViewBuilder, and generics. Two of those — some and any — are keywords that appear in every public API and SwiftUI view you write. They're easy to confuse. They both appear next to protocols. They both solve the "protocol as type" problem. But they represent fundamentally different trade-offs.
This post covers what each one means, how they're implemented, and when to use which.
The Setup
Start with a simple protocol:
swiftprotocol Shape { func area() -> Double } struct Circle: Shape { let radius: Double func area() -> Double { .pi * radius * radius } } struct Square: Shape { let side: Double func area() -> Double { side * side } }
If you try to return Shape from a function in Swift 6+:
swiftfunc makeShape() -> Shape { // ❌ compiler error in Swift 6 Circle(radius: 5) }
The compiler rejects it:
Use of protocol 'Shape' as a type must be written 'any Shape'
Using a protocol as a plain type and using it as an opaque or existential type are different things. Swift 6 requires explicit syntax: some Shape or any Shape, not bare Shape.
Opaque Types — some
What It Means
some Shape means: there is one specific concrete type here, but the caller doesn't need to know which one.
With some, the compiler knows the concrete type. The caller sees only the protocol interface, but the compiler has complete type information. The guarantee isn't hidden from the type system — only the concrete type is hidden from the caller.
swiftfunc makeShape() -> some Shape { Circle(radius: 5) }
The caller gets back something that conforms to Shape. They can call area() on it. What they can't do is assume it's a Circle specifically. You might change the implementation to return a different shape tomorrow, and their code stays valid.
But the compiler knows. It resolved some Shape to Circle at compile time. That matters for performance.
The Same-Type Rule
A function returning some Protocol must return the same concrete type from every return path:
swiftfunc makeShape(big: Bool) -> some Shape { if big { return Circle(radius: 100) // ✅ } else { return Square(side: 10) // ❌ compiler error } }
Error:
Function declares an opaque return type 'some Shape', but the return statements in its body do not have matching underlying types
This isn't an arbitrary restriction. some Shape means one specific type. The compiler encodes that type into the function's signature. If two different branches could return different types, the guarantee breaks down. The caller's code compiled against the specific type would no longer be valid.
If you need to return different types, that's what any is for.
When some is too strict: Different conditional branches return different view types:
swift// ❌ Compiler error — branches return different types func icon(for status: Status) -> some View { switch status { case .pending: return Image(systemName: "clock") // Image case .done: return Image(systemName: "checkmark") // Image case .error: return Text("Error") // Text — different type! } }
Solutions:
swift// ✅ Use @ViewBuilder — compiler wraps branches in _ConditionalContent @ViewBuilder func icon(for status: Status) -> some View { switch status { case .pending: Image(systemName: "clock") case .done: Image(systemName: "checkmark") case .error: Text("Error") } } // ✅ Use any View if branches are fundamentally incompatible func icon(for status: Status) -> any View { switch status { case .pending: return Image(systemName: "clock") case .done: return Image(systemName: "checkmark") case .error: return Text("Error") } }
@ViewBuilder is preferred — it keeps the concrete type information (each branch is encoded in _ConditionalContent<A, B>), so SwiftUI can still diff correctly.
some in SwiftUI
This is where you may have encountered opaque types first:
swiftstruct ContentView: View { var body: some View { VStack { Text("Hello") Image(systemName: "star") } } }
body returns some View. The compiler resolves this to VStack<TupleView<(Text, Image)>> under the hood. That specific type is what SwiftUI uses to track view identity across renders. Knowing the structural type is how it can diff the view tree without a virtual DOM.
some as a Parameter Type
some isn't only for return types. You can use it for parameters too (Swift 5.7+):
swiftfunc printArea(_ shape: some Shape) { print(shape.area()) }
This is shorthand for a generic function. The compiler desugars it to:
swiftfunc printArea<T: Shape>(_ shape: T) { print(shape.area()) }
The two forms are equivalent in behavior and performance. Use some when the type variable doesn't appear in other parameters or the return type. When you need the type to appear multiple times (e.g., func compare<T: Comparable>(_ a: T, _ b: T) -> T), generics are clearer.
Existential Types — any
What It Means
any Shape means: this value conforms to Shape, and that's all we know — the concrete type is determined at runtime.
Unlike some, the compiler does not know the underlying type. It allocates a runtime container that can hold any conforming value, and dispatches method calls through an indirection table. This lookup cost is why any should be a explicit choice, not a default fallback.
swiftfunc randomShape() -> any Shape { Bool.random() ? Circle(radius: 5) : Square(side: 5) }
This compiles fine. The function can return different concrete types on different calls. The trade-off is that the concrete type is unknown at compile time.
How the Existential Container Works
When Swift creates a value of type any Shape, it builds an existential container. On 64-bit platforms, this is a 5-word (40-byte) box:
┌────────────────────────────────────────┐
│ Value buffer (3 words / 24 bytes) │ ← inline storage for small values
│ Type metadata pointer (1 word / 8 bytes) │ ← runtime metadata for concrete type
│ Witness table pointer (1 word / 8 bytes) │ ← function pointers to implementations
└────────────────────────────────────────┘
Value storage. Small values (up to three pointer-sized words) fit in the inline buffer. Larger values spill to heap allocation. Circle with one Double fits inline; a struct with four fields does not.
Type metadata. A pointer to the runtime metadata for the concrete type — determines size, alignment, and how to copy or destroy the value.
Witness table. A table of function pointers, one per protocol requirement, wired to the concrete type's implementations. When you call shape.area() on an any Shape, Swift looks up the area pointer in the witness table and calls through it—identical to how vtables work for classes. Each entry is a function pointer to the concrete type's implementation.
This runtime machinery provides flexibility and is the source of the dispatch cost.
Heterogeneous Collections
The main use case for any is heterogeneous storage — a collection that holds multiple conforming types at once:
swiftlet shapes: [any Shape] = [ Circle(radius: 3), Square(side: 4), Circle(radius: 10) ] for shape in shapes { print(shape.area()) // witness table dispatch on each call }
You can't do this with some. [some Shape] would require every element to be the same concrete type, which defeats the purpose of a heterogeneous list.
any with Associated Types
Before Swift 5.7, protocols with associated types couldn't be used as existentials at all. With primary associated types, you can constrain existentials:
swiftprotocol Container<Element> { associatedtype Element func items() -> [Element] } func display(_ c: any Container<String>) { c.items().forEach { print($0) } }
any Container<String> says: give me any container whose Element is String. The concrete container type is unknown — could be an array wrapper, a database cursor, anything — but the element type is fixed. This is a constrained existential.
Common iOS pattern: Mixing some inside a protocol with any at the call site:
swiftprotocol View { associatedtype Body: View @ViewBuilder var body: some View { ... } // ← some View inside } var screens: [any View] = [...] // ← any View at use site for screen in screens { let body = screen.body // ← body type is unknown, but it conforms to View }
This pattern is how SwiftUI navigates the same-type constraint: the protocol promises opaque (concrete) bodies, but a collection of heterogeneous view types needs existential treatment.
The Performance Gap
The performance difference between some and any comes down to dispatch.
With some (and generics), the compiler resolves the concrete type at compile time. It can:
- inline the call entirely
- use direct function call rather than indirection
- specialize the code for the specific type
With any, none of that is possible. Every method call goes through the witness table. Small values might avoid heap allocation if they fit in the inline buffer, but the dispatch indirection is always there.
For a tight loop calling a protocol method millions of times, the witness table overhead is measurable. In SwiftUI rendering a 100-item list, the dispatch cost is <0.1ms per frame — beneath the noise. Profile before optimizing any away.
Prefer some because it tells the compiler more. This enables better error messages, earlier compile-time error detection, and leaves room for future optimization without API changes.
some vs any vs Generics
There's a third option for every situation where you might reach for some or any: an explicit generic constraint.
swift// Generic func printArea<T: Shape>(_ shape: T) { print(shape.area()) } // Opaque parameter (same thing, shorter) func printArea(_ shape: some Shape) { print(shape.area()) } // Existential parameter func printArea(_ shape: any Shape) { print(shape.area()) }
The first two are equivalent. The third is different.
For return types the equivalence breaks down. some Shape as a return type means one specific concrete type whose identity is hidden. A generic return type <T: Shape> -> T means the caller picks the type — the opposite. These are fundamentally different function signatures.
| Who picks the type | Dispatch | Use for | |
|---|---|---|---|
<T: Shape> parameter | caller | static | reusable algorithms |
some Shape parameter | callee | static | same as generics, cleaner syntax |
any Shape parameter | runtime | dynamic | receive different types in different calls |
some Shape return | callee (hidden from caller) | static | hiding implementation details |
<T: Shape> return | caller (determines T) | static | caller controls the concrete type |
any Shape return | runtime | dynamic | heterogeneous storage |
SwiftUI: some View vs AnyView
AnyView is the existential container for SwiftUI views. It's essentially any View wrapped in a type-erased box (SwiftUI predates the any keyword).
swift// ✅ preferred — compiler resolves _ConditionalContent<Text, Image> at compile time @ViewBuilder func badge(premium: Bool) -> some View { if premium { Text("Pro") } else { Image(systemName: "star") } } // ⚠️ BREAKS VIEW IDENTITY — concrete type is lost, reconciler loses structural knowledge func badge(premium: Bool) -> AnyView { if premium { AnyView(Text("Pro")) } else { AnyView(Image(systemName: "star")) } }
AnyView destroys SwiftUI's structural type tree. The reconciler cannot identify whether the AnyView on screen is the same view as the previous frame — it must blindly re-evaluate. In lists with hundreds of cells, this forces unnecessary re-rendering and can degrade performance significantly.
@ViewBuilder solves the conditional-type problem without erasure. It produces a concrete _ConditionalContent<A, B> type at compile time, encodes both branches in the type, and lets SwiftUI diff correctly.
The practical rule: Avoid AnyView unless you're building something genuinely dynamic — like a dynamic view registry or a theme system where view types aren't known until runtime.
When to Use Each
Reach for some when you're returning a protocol type and the concrete type is always the same — the caller just doesn't need to know what it is. This is the default for return types in Swift. It's free, it's type-safe, and it lets you change the implementation later without touching call sites.
swiftfunc makeButton() -> some View { ... } func defaultAnimal() -> some Animal { ... }
Use a generic when you're writing a reusable algorithm that works over any conforming type and you need to reference the type elsewhere in the signature. For simple single-parameter cases some is cleaner and identical.
swiftfunc largest<T: Comparable>(_ a: T, _ b: T) -> T { a > b ? a : b }
Reach for any when you genuinely need to store values of different conforming types together, or when the type isn't known until runtime.
swiftvar plugins: [any Plugin] = [] var shapes: [any Shape] = []
If you find yourself writing any for a return type that always returns the same concrete thing, switch to some. If you find yourself writing any for a parameter that doesn't need heterogeneous inputs, switch to some or a generic.
The Mental Model
A way of thinking you can follow is:
some = I know which type this is. You don't.
any = Neither of us knows until runtime.
some is a compile-time guarantee that gets erased from the API surface. any is a runtime box that can hold different things in different calls.
When Swift 5.7 added the requirement to write any explicitly (rather than using a bare protocol name), the goal was to make dynamic dispatch visible at the call site. Seeing any Shape in a function signature signals: there's a witness table lookup happening here. Seeing some Shape signals: the compiler resolved this statically at compile time.
That explicitness forces you to think about dispatch costs while reading code.