Articles 📜, notes 📒, experiments 🤯 about iOS Development

Custom Subscripts in Swift

Subscript is a powerful API for accessing or settings elements of a collection, list, or sequence that can be defined with classes, structures, and enumerations. You might be familiar with subscript usage from arrays and dictionaries:

let firstImage = images[0]
let cachedItem = cache["url"] 

We can also declare own subscripts to access or set elements.

Declaration

Let's take a look declaration of subscript in an array:

subscript(index: Int) -> Element { get set }

To be able to define a subscript, we use subscript keyword and define parameters and output type. Subscripts can take any number of input parameters of any type and can return any type just like functions. Also, subscripts can have default parameters, but can't have in-out parameters.

An instance or type can define read-only and read-write subscripts. In the example above, we will be declaring two subscripts, one for reading value from the instance, and one for settings value to the instance.

struct TodoItem {
    let title: String = ""
}

final class TodoListStore {
    static let shared = TodoListStore()
    
    // Represents todo items under list id.
    private var lists: [String: [TodoItem]] = [:]
    
    // Avoiding new instance creation.
    private init() { }
}

In the example above, there is a singleton class, TodoListStore, that stores an array of todo lists. Ideally, we can declare a method to retrieve a specific TodoItem list using method. We can also define a subscript that takes an id and returns todo items, so these both will do the same thing:

extension TodoListStore {
    subscript(listId: String) -> [TodoItem]? {
        return lists[listId]
    }
    
    func retrieveItems(for listId: String) -> [TodoItem]? {
        return lists[listId]
    }
}

// Subscript usage:
let items = TodoListStore.shared["id-1"]

// Method usage:
let items = TodoListStore.shared.retrieveItems(for: "id-1")

Note that get block is not needed if it is read-only subscript.

Overloading

The definition of multiple subscripts is known as subscript overloading, we can define any number of subscripts. Let's add new subscript to our TodoListStore to have to set a list to a specified list id.

extension TodoListStore {
    subscript(items: [TodoItem], listId listId: String) -> [TodoItem]? {
        get {
            // Calls `subscript(listId: String) -> [TodoItem]?` subscript.
            self[listId]
        } set {
            lists[listId] = items
        }
    }
}

let todoItem = TodoItem()
TodoListStore.shared[[todoItem], listId: "id-1"]

Notice that we can have argument labels with subscript implementation to make it easy to understand and we can define multiple parameters. Subscripts can be either read-only or read-write, that's why our subscript has get block.

Type Subscripts

A type can define subscript as well, consider the example above, we have a remote config class that contains all config flags:

class RemoteConfig {
    enum Flag {
        case isIAPEnabled
        case isOnboardingEnabled
    }
    
    static private let shared = RemoteConfig()
    
    // Avoiding new instance creation.
    private init() { }
    
    private var flags: [Flag: Bool] = [.isOnboardingEnabled: true]
}

Let's add a type subscript to get flag value from the shared instance:

extension RemoteConfig {
    static subscript(flag: Flag) -> Bool? {
        return RemoteConfig.shared.flags[flag]
    }
}

// Usage:
let flag = RemoteConfig[.isOnboardingEnabled]

Notice that in this way, we avoid exposing the shared instance. Also, you can use class keyword instead of static keyword to have override ability for subclasses.

Safe Index Operations with Subscript

Accessing a non-existing index in an array causes a crash, let's avoid this behavior using subscript:

public extension Array {
    subscript(safeIndex index: Int) -> Element? {
        guard index >= 0, index < endIndex else { return nil }
        return self[index]
    }
}

Conclusion

Subscript is powerful for accessing or settings element, we can declare an unlimited number of subscripts, we can inherit them, and define type subscripts. But over subscript usage may lead to confusion, and can decrease readability. It is best to keep subscript usage limited to access or set data for simple cases. For more complex usage requirements, I recommend to use methods.

Tagged with: