Weekly Swift articles, podcasts and tips by John Sundell.

How to create an array with mixed types in Swift?

Answered on 21 Jul 2020
Basics article available: Type inference

Arrays that contain instances of multiple types are often referred to as heterogenous arrays, and are incredibly commonly used in dynamically typed languages — like PHP, JavaScript, and Ruby. However, in statically typed languages (like Swift), modeling such arrays in a neat and clean way can at times be quite challenging.

For example, let’s say that we’re working on an app that displays two main kinds of content — videos and photos — and that we’ve implemented those variants as two separate model types that both contain shared properties, as well as data that’s unique to each variant:

struct Video {
    let id: UUID
    var title: String
    var description: String
    var url: URL
    var length: TimeInterval
}

struct Photo {
    let id: UUID
    var title: String
    var description: String
    var url: URL
    var size: CGSize
}

While creating an array that contains instances of just one of the above two types is really straightforward — we just use either [Video] or [Photo] — if we‘d like to mix instances of both types within the same array, things instantly get much more complicated.

In those kinds of situations, one option would be to circumvent Swift’s strong type system entirely, by using Any (which literally means any type) as the element type for the array that we’re looking to create — like this:

func loadItems() -> [Any] {
    var items = [Any]()
    ...
    return items
}

That’s not really great, since we’d always have to perform type casting in order to actually use any of the instances that our returned array contains — which both makes things much more clunky, and more fragile, as we don’t get any compile-time guarantee as to what sort of instances that our array will actually contain.

Another option would be to use a bit of protocol-oriented programming, and abstract the parts that are common between both Video and Photo into a protocol that we make both types conform to:

protocol Item {
    var id: UUID { get }
    var title: String { get }
    var description: String { get }
    var url: URL { get }
}

extension Video: Item {}
extension Photo: Item {}

That’s much better, since we can now give our loadItems function a strong return type, by replacing Any with the above Item protocol — which in turn lets us use any property defined within that protocol when accessing our array’s elements:

func loadItems() -> [Item] {
    ...
}

However, the above approach can start to become a bit problematic if we at some point need to add a requirement to Item that makes it generic — for example by making it inherit the requirements of the standard library’s Identifiable protocol (which contains an associated type):

protocol Item: Identifiable {
    ...
}

Even though both Video and Photo already satisfy our newly added requirement (since they both define an id property), we’ll now end up getting the following error whenever we try to reference our Item protocol directly, like we do within our loadItems function:

Protocol 'Item' can only be used as a generic constraint
because it has Self or associated type requirements.

For more information about the above error and its underlying cause, check out the answer to “Why can’t certain protocols, like Equatable and Hashable, be referenced directly?”

One way to work around that problem in this case would be to separate the requirement of conforming to Identifiable from our own property requirements — for example by moving those properties to an AnyItem protocol, and to then compose that new protocol with Identifiable in order to form an Item type alias:

protocol AnyItem {
    var id: UUID { get }
    var title: String { get }
    var description: String { get }
    var url: URL { get }
}

typealias Item = AnyItem & Identifiable

The beauty of the above approach is that our Video and Photo types can still conform to Item, just like before, which makes them compatible with all code that expects Identifiable instances (such as SwiftUI’s ForEach and List) — while we can now also refer to AnyItem whenever we want to mix both videos and photos within the same array:

func loadItems() -> [AnyItem] {
    ...
}

An alternative to the above would be to instead use an enum to model our two separate variants, like this:

enum Item {
    case video(Video)
    case photo(Photo)
}

Since both Video and Photo use the same type for their id properties (the built-in UUID type), we could even make the above enum conform to Identifiable as well — by making it act as a proxy for the underlying model instance that it’s currently wrapping:

extension Item: Identifiable {
    var id: UUID {
        switch self {
        case .video(let video):
            return video.id
        case .photo(let photo):
            return photo.id
        }
    }
}

Another variation of the above pattern would be to implement Item as a struct instead, by moving all the properties that are common between our two variants into that struct, and to then only use an enum for the properties that are unique to either variant — like this:

struct Item: Identifiable {
    let id: UUID
    var title: String
    var description: String
    var url: URL
    var metadata: Metadata
}

extension Item {
    enum Metadata {
        case video(length: TimeInterval)
        case photo(size: CGSize)
    }
}

Either of the last two approaches have the advantage that they actually let us turn our heterogenous arrays into homogenous ones, since Item is now implemented as a stand-alone type, which means that we can now pass those arrays into functions that require all elements to be of the same type — like this one from the Basics article about generics:

extension String {
    mutating func appendIDs<T: Identifiable>(of values: [T]) {
        for value in values {
            append(" \(value.id)")
        }
    }
}

So that’s a few different ways to implement arrays with mixed types in Swift. I hope that you found this answer useful, and if you’d like to learn more about this topic, then I’d recommend reading “Handling model variants in Swift”, which goes through some of the above techniques in much more detail.

Thanks for reading! 🚀

Support Swift by Sundell by checking out this sponsor:

Instabug
Instabug

Instabug: Investigate, diagnose and resolve issues up to four times faster. Whether it’s a crash, slow screen transitions, slow network calls or unresponsive UIs, Instabug lets you utilize powerful performance patterns to trace the cause of each issue. Detect if a specific app version, device or network connection is affecting the user experience and spot trends and spikes. Get started now and ship apps that your users will love.