Swift
Protocols
Array Types
Function \`Parameters\`
Programming

Usage of protocols as array types and function parameters in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Protocols are one of Swift's main tools for writing code against behavior instead of concrete types. They become especially useful when you want a function to accept multiple implementations or when you need a collection that can hold different types that still share the same capabilities.

Protocols as function parameters

A protocol describes the operations a value must support. When a function takes a protocol type, the caller can pass any conforming value.

In current Swift, an existential protocol value is written with any. That makes the intent explicit: the function accepts any concrete type that conforms to the protocol.

swift
1protocol Drawable {
2    func draw() -> String
3}
4
5struct Circle: Drawable {
6    func draw() -> String {
7        "drawing a circle"
8    }
9}
10
11struct Square: Drawable {
12    func draw() -> String {
13        "drawing a square"
14    }
15}
16
17func render(_ item: any Drawable) {
18    print(item.draw())
19}
20
21render(Circle())
22render(Square())

This works well when the function only needs the protocol's declared interface. The function does not care whether the caller passes a struct, class, or enum.

Protocols as array element types

The same idea applies to collections. An array of any Drawable can hold multiple concrete types as long as they all conform to Drawable.

swift
1protocol Drawable {
2    func draw() -> String
3}
4
5struct Triangle: Drawable {
6    func draw() -> String {
7        "drawing a triangle"
8    }
9}
10
11struct TextLabel: Drawable {
12    let text: String
13
14    func draw() -> String {
15        "drawing text: \(text)"
16    }
17}
18
19let items: [any Drawable] = [Triangle(), TextLabel(text: "Hello")]
20
21for item in items {
22    print(item.draw())
23}

This gives you a heterogeneous array with a shared interface. That is useful for view models, rendering pipelines, plugin points, and command dispatch systems.

When generics are a better fit

Using any Protocol introduces existential storage and dynamic dispatch. That is often acceptable, but sometimes you want a function to preserve the caller's concrete type. In that case, a generic constraint is better.

swift
1protocol IdentifiableItem {
2    var id: String { get }
3}
4
5struct User: IdentifiableItem {
6    let id: String
7}
8
9func firstID<T: IdentifiableItem>(in items: [T]) -> String? {
10    items.first?.id
11}
12
13let users = [User(id: "u1"), User(id: "u2")]
14print(firstID(in: users) ?? "missing")

This version requires every array element to have the same concrete type, but it avoids existential boxing and allows the compiler to keep more type information.

Protocols with associated types or Self requirements

Not every protocol can be used directly as an array type or parameter type. If a protocol has an associated type or uses Self in a way that ties behavior to the conforming type, the compiler may reject existential usage.

swift
1protocol Parser {
2    associatedtype Output
3    func parse(_ input: String) -> Output?
4}

You cannot usually write [any Parser] and then call parse in a useful way, because each parser may produce a different output type. In these cases, you normally use generics, a type-erased wrapper, or a more specific protocol design.

Choosing between arrays of protocols and arrays of concrete types

Use an array of protocol values when you need to mix implementations. Use an array of a concrete type when every element is the same and the extra abstraction adds no value.

For example, a menu system that stores buttons, toggles, and sliders behind one Interactable protocol is a good match for [any Interactable]. A list of users fetched from an API should probably stay [User].

Common Pitfalls

A frequent mistake is omitting any in modern Swift examples. Many code samples written before the newer syntax still compile in some contexts, but current Swift style expects existential protocol values to be explicit.

Another common issue is trying to store protocols with associated types directly in arrays. If the protocol describes values with different output types, existential use becomes limited or impossible.

It is also easy to overuse protocols when a generic function or a concrete model would be simpler. If there is only one implementation today and no real abstraction boundary, protocol-based design may add indirection without a clear benefit.

Summary

  • Protocols let functions accept behavior-based inputs rather than concrete types.
  • Use any ProtocolName for existential parameters and heterogeneous arrays in modern Swift.
  • Arrays like [any Drawable] are useful when different types share the same interface.
  • Prefer generics when all items have the same concrete type and you want stronger compile-time guarantees.
  • Protocols with associated types often require generics or type erasure instead of direct existential storage.

Course illustration
Course illustration

All Rights Reserved.