Greetings, traveler!
When preparing for iOS interviews, developers eventually reach a group of questions that appear deceptively simple. Generics, protocols with associated types, existential types, and the keywords some and any often appear in that category. The syntax is familiar to most Swift engineers. The underlying model of the type system is where interviews become interesting.
Swift’s abstraction mechanisms form a layered system. Each layer trades type information for flexibility in a different way.
Generics in Swift
Generics allow functions and types to operate on values whose concrete type is determined later. The compiler still keeps full type information, which allows strong compile-time guarantees and optimization.
A typical example appears in the standard library:
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}The placeholder T represents a concrete type that the caller supplies. The function works with integers, strings, custom structs, or any other type, as long as both parameters share the same type.
The important detail lies in what happens at the call site. When the compiler sees
var a = 1
var b = 2
swapValues(&a, &b)the generic parameter resolves to Int. Inside that invocation the function operates on a fully known concrete type.
Generics also allow relationships between types to be expressed in the type system. This ability becomes essential when working with collections.
func containsSameElements<C1: Collection, C2: Collection>(
_ lhs: C1,
_ rhs: C2
) -> Bool
where C1.Element == C2.Element, C1.Element: Equatable {
Array(lhs) == Array(rhs)
}The where clause expresses two constraints. The elements of both collections must have the same type, and those elements must conform to Equatable. Swift checks these relationships during compilation. No runtime checks are required.
The key idea is that generics preserve type information. The compiler knows the exact type behind the abstraction.
Protocol-based abstraction
Protocols provide another mechanism for abstraction. Instead of parameterizing over a type, a protocol describes a set of capabilities that a type must implement.
protocol Logger {
func log(_ message: String)
}Any type that implements log(_:) can conform to Logger. The protocol becomes a shared interface.
Protocol abstraction works well when several concrete implementations share a common behavior. A networking client, a file logger, and a console logger may all conform to the same protocol while providing different implementations.
In interview discussions this often leads to the question of when protocols and generics should be used.
Generics preserve concrete type information. Protocols describe capabilities while allowing the concrete type to vary behind the interface. The distinction becomes clearer when we look at protocol existentials.
Protocols with associated types
Many protocols need to describe relationships between types. Swift solves this with associatedtype.
A simple example illustrates the idea:
protocol Repository {
associatedtype Entity
func save(_ entity: Entity)
func loadAll() -> [Entity]
}Entity acts as a placeholder type that conforming implementations must define.
struct UserRepository: Repository {
func save(_ entity: User) { }
func loadAll() -> [User] {
...
}
}The protocol expresses a relationship between the parameters and return types of its methods. Both methods operate on the same entity type.
This pattern appears throughout the Swift standard library. Sequence, Collection, and IteratorProtocol all rely on associated types. The design allows the protocol to remain abstract while preserving relationships between types.
Generic constraints and protocol relationships
Once a protocol includes associated types, generics become the primary tool for using that protocol in APIs.
Consider the Sequence protocol. It declares an associated type called Element.
A generic function can express requirements involving that type:
func printElements<S: Sequence>(_ sequence: S)
where S.Element: CustomStringConvertible {
for element in sequence {
print(element.description)
}
}The compiler knows the exact element type of the sequence and checks the constraint during compilation.
This relationship between generics and protocols forms the backbone of many Swift APIs.
Existential types and any
Swift also allows a protocol to be used directly as a type. This usage is called an existential.
Starting with Swift 5.7 the syntax became explicit:
let logger: any LoggerThe keyword any signals that the value stored in the variable may have different concrete types at runtime, as long as those types conform to the protocol.
This introduces a different abstraction model. Instead of preserving type information, an existential hides the concrete type inside a container.
let items: [any CustomStringConvertible] = [
42,
"Hello",
true
]Each element has a different concrete type. The array stores them as values conforming to the same protocol.
The price of this flexibility is a loss of type information. The compiler only knows that each value conforms to the protocol. Operations specific to the concrete type become unavailable without casting.
Existential values also involve an additional level of indirection. The container holds both the value and metadata describing the conforming type.
Limitations of existential types
Protocol existentials introduce several constraints that frequently appear in interview questions.
The first limitation relates to associated types. When a protocol declares an associated type, the existential cannot expose a single concrete type for that placeholder.
Consider a simplified protocol:
protocol Storage {
associatedtype Item
func store(_ item: Item)
}A value of type any Storage hides the concrete implementation. The compiler cannot determine which Item type should be used. The protocol’s methods therefore become difficult or impossible to call through the existential.
Another limitation appears when protocol requirements involve Self. Methods returning Self or using it in complex positions often cannot be accessed through an existential value.
These restrictions explain why generics remain the preferred approach when the relationships between types matter.
Opaque types and the some keyword
Swift introduced opaque types to address a different design problem. Sometimes a function needs to return a value conforming to a protocol while keeping the concrete type hidden from the caller.
This is where the keyword some appears.
func makeShape() -> some Shape {
Circle()
}The function returns a concrete type that conforms to Shape. The exact type remains hidden in the public interface.
The compiler still knows the concrete type. That allows static dispatch and full optimization.
The implementation chooses the concrete type while callers interact with the value through the protocol interface.
An important rule governs opaque return types. Every return path of the function must produce the same concrete type.
func makeShape(flag: Bool) -> some Shape { // ❌ Error
if flag {
return Circle()
} else {
return Square()
}
}This code fails to compile because two different concrete types appear in the return paths.
Each opaque type corresponds to a single hidden type chosen by the implementation.
Generics, some, and any compared
These three mechanisms represent different levels of abstraction in Swift.
Generics preserve full type information. The caller chooses the concrete type, and the compiler verifies all constraints at compile time.
Opaque types hide the concrete type from callers while keeping it fixed within the implementation.
Existentials erase the concrete type entirely and allow values of multiple conforming types to coexist.
A simple mental model helps during interviews.
Generics allow the caller to decide the concrete type.
Opaque types allow the implementation to decide the concrete type.
Existentials allow the concrete type to vary at runtime.
Understanding these distinctions makes many interview questions much easier to answer.
A practical example from SwiftUI
SwiftUI provides a well-known example of opaque types.
var body: some ViewA SwiftUI view often expands into a deeply nested structure composed of multiple view types. Exposing that concrete type in the API would produce unreadable type signatures.
Opaque types allow SwiftUI to keep the actual type hidden while preserving compile-time optimization.
The compiler knows the full structure of the view hierarchy. The developer interacts with it through the View protocol.
Choosing the right abstraction
Choosing between generics, opaque types, and existentials depends on the role of the abstraction in the API.
Generics work well when relationships between types matter and when maximum type safety is required.
Opaque types suit APIs that return a specific implementation while keeping that implementation hidden.
Existentials work well when heterogeneous values must share the same container or interface.
Conclusion
The language offers several abstraction tools, each designed to balance flexibility, type safety, and performance in different ways.
In practice most Swift codebases use all three approaches. The key skill lies in recognizing which level of abstraction best matches the problem being solved.
