How to create an array with mixed types in Swift?
Basics article available: Type inferenceArrays 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! 🚀