Custom subscripts in Swift explained with code examples

Custom subscripts in Swift allow you to write shortcuts to elements from collections or sequences and can be defined within classes, structures, and enumerations. You can use subscripts to set and retrieve values without exposing the inner details of a certain instance.

An instance can define multiple subscripts and a subscript can have multiple input parameters. It’s likely that you’ve already been using subscripts to, for example, get elements from an array or dictionary. However, it’s less likely that you also defined your own custom subscripts while those can be extremely useful as well.

What is a subscript?

A subscript defines a shortcut to elements of a collection, list, or sequence. It can be defined in classes, structures, and enumerations to allow quick access to elements from a certain type.

It’s commonly used throughout the standard Swift library to, for example, access elements from an array:

var array = ["Antoine", "Jaap", "Lady"]
print(array[0]) // Prints: Antoine

Or to access elements from a dictionary:

var agesDictionary = ["Antoine": 29, "Lady": 2, "Jaap": 1]
print(agesDictionary["Antoine"]) // Prints: 29

The same subscript can also be used to add new values:

array[0] = "Henk"
print(array[0]) // Prints: Henk

agesDictionary["Antoine"] = 30
print(agesDictionary["Antoine"]) // Prints: 30

Without those subscripts, you would likely access the same through defined methods on the array or dictionary. Those methods can easily make your code look a lot less clean:

array.set("Henk", forIndex: 0)
agesDictionary.set(30, forKey: "Antoine")

Note that those methods don’t actually exist as we now have subscripts in Swift to do the same.

Taking the above example as an improvement is also the way that you can take a look at your own code. Whenever you find yourself writing similar methods you can think about writing your own custom subscript instead.

Creating a custom subscript

Custom subscripts can be defined as if you define a method in your class, structure, or enumeration. Before we dive into this logic, I’d like to introduce an image cache instance that we’re going to improve with subscripts.

final class ImageCache {
    static let shared = ImageCache()

    private var imageStore: [URL: UIImage] = [:]

    func image(for url: URL) -> UIImage? {
        imageStore[url]
    }

    func store(_ image: UIImage, for url: URL) {
        imageStore[url] = image
    }
}

This ImageCache class is responsible for caching images for a given URL. We have a method to add an image to the cache and a method to retrieve an image from the cache.

For this example, we will store the SwiftLee logo so we can retrieve that later on with the subscripts. If you’re writing this code along, you might want to do the same with your own image.

let swiftLeeLogo = UIImage(named: "logo_256x256.jpg")!
let swiftLeeURL = URL(string: "https://www.avanderlee.com/logo.png")!
ImageCache.shared.store(swiftLeeLogo, for: swiftLeeURL)

For the sake of this example, we’re statically caching an image for a self-defined URL. Normally, you would implement such a cache based on an actual image request you did with URLSession and save the response image.

Now that we’ve set up our image cache we can start writing our first custom subscript.

extension ImageCache {
    subscript(url: URL) -> UIImage? {
        imageStore[url]
    }
}

The code looks really simple:

  • It defines the custom subscript by using the subscript method name
  • The subscript takes a URL as input
  • The result will be an image if it exists

We basically make use of the image store that we defined before. This image store is a dictionary that takes the URL as a key and the image as the value. This allows for a simple way of fetching images based on a URL.

We can make use of this subscript as follows:

let swiftLeeURL = URL(string: "https://www.avanderlee.com/logo.png")!
let image = ImageCache.shared[swiftLeeURL]

Making a read and write subscript

The above subscript example allows you to retrieve values only and can be seen as a read-only subscript. We can easily make this subscript writable as well by adding the get and set closures:

extension ImageCache {
    subscript(url: URL) -> UIImage? {
        get {
            imageStore[url]
        }
        set {
            imageStore[url] = newValue
        }
    }
}

This results in the fact that we can now store new images by making use of the subscript:

let avatarImage = UIImage(named: "avatar.jpg")!
let avatarURL = URL(string: "https://www.avanderlee.com/avatar.png")!

ImageCache.shared[avatarURL] = avatarImage

Defining static subscripts

Static subscripts allow you to write shortcuts by using a subscript on a static type. This allows you to expose even less of your instance.

In our image cache example, we have defined a shared instance by defining the shared property. Without a static subscript, we have to always access the subscript through the shared property:

ImageCache.shared[swiftLeeURL]

This also requires us to expose the shared property while this might not be intended at first.

We can solve this by defining a static subscript in which we access the shared instance:

extension ImageCache {
    static subscript(url: URL) -> UIImage? {
        get {
            shared.imageStore[url]
        }
        set {
            shared.imageStore[url] = newValue
        }
    }
}

The code is nearly the same. In fact, the only thing we changed is adding the static keyword and accessing the imageStore through the shared property.

We can now make our shared property private and retrieve images for a URL through the static subscript:

// Without the static subscript:
let image = ImageCache.shared[swiftLeeURL]
ImageCache.shared[swiftLeeURL] = newImage

// With the static subscript:
let image = ImageCache[swiftLeeURL]
ImageCache[swiftLeeURL] = newImage

This is great and allows us to expose as few implementation details as possible.

Adding an optional parameter to a subscript

Optional parameters in a subscript allow you to add extra functionality. We could, for example, make it possible to fetch an image based on the base URL only:

extension ImageCache {
    subscript(url: URL, useBaseURL: Bool = false) -> UIImage? {
        if useBaseURL {
            return imageStore.first { (storedImage) -> Bool in
                storedImage.key.baseURL == url.baseURL
            }?.value
        } else {
            return imageStore[url]
        }
    }
}

Note that we made this subscript read-only as we don’t want to allow our users of the image cache to save an image for the base URL only. It’s better to save an image for a specific URL instead.

We can now retrieve the same stored SwiftLee logo by only using the base URL:

let image = ImageCache.shared[URL(string: "https://www.avanderlee.com")!, true]

// Passing false will try to find an image for the exact URL, which fails as we did not store an image for that URL.
let nilImage = ImageCache.shared[URL(string: "https://www.avanderlee.com")!, false]

This is a great way to add extra functionality to your custom subscripts.

Conclusion

Custom subscripts are a great way to make your code more readable while at the same time disclosing more implementation details. Subscripts are available throughout the Swift library for instances like arrays and collections and give you a shortcut for setting and retrieving values. You can define custom subscripts on structures, classes, and enumerations.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!