A Swift Tip: Extending Arrays To Do Whatever We Want

Posted

Let's look into how Swift makes it super easy to add our own custom methods to Array objects (and any other object). As I mentioned before, I'm currently building a game for iOS with Swift. As I work through this project, and am learning the Swift for myself, I'll try and share some tips here.

My game needs to manage a number of lists of items, and frequently needs to pull random elements from them. The code to accomplish this is fairly straight-forward. Let's say, for example, we have an array of Tile objects,

var tiles: [Tile]

If we know the index we want to remove, we can easily remove the element at that index by using the Array.remove(at: Int) method. But in this case, we don't know the index we want to remove, we just want any index.

Fortunately every Swift Array object has an .indices property which is an(other) Array with all the indexes of the original array. So we can grab a random index from the tiles.indices property via the Array.randomElement() method,

let randomTileIndex: Int? = tiles.indices.randomElement()

Now that we've got an index, we can remove the random element from our array at this index using the previously mentioned .remove(at:) method,

if (randomTileIndex != nil) {
    let randomTile: Tile = tiles.remove(at: randomTileIndex!)
}

This is pretty straight forward to implement. But wouldn't be great if every Array just had removeRandomElement() as a method already so we didn't have to repeat these 3-4 lines every time we need a random element? This is where Swift's Extensions come in, allowing us to add a method directly to All Arrays Everywhere*!!!

*: at least within our project.

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you don’t have access to the original source code (known as retroactive modeling).

Let's walk through adding a new removeRandomElement() method to All Arrays Everywhere together.

A common convention I've seen in a number of projects is to create your extensions in a new folder named "Extensions", and to name your extension file following this template: <Type Name>+<New Property/Method Name>.swift, e.g. Extensions/Array+RemoveRandomElement.swift.

In the extension file, we first declare we're adding an extension to a specific type, e.g. Array,

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    
}

Within this extension block we add our new properties or methods. Lets add the removeRandomElement method,

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    func removeRandomElement() {
    
    }
}

First things first, we have to declare a return type for this method. Since this is method is being added to the the abstract Array type, can't just say this method will return a Tile object - this won't work for All Arrays Everywhere!!! Instead we have to declare that this method will return an element of whatever type the Array has been specified to contain. Fortunately the Array type has a typealias called Element that we can use as a placeholder wherever we need to say "whatever type the array has been specified to use". We can access it in our new method signature using the Self.Element,

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    func removeRandomElement() -> Self.Element {
    
    }
}

Almost there! We have 2 last things we need to make this method work properly. First we need to handle the case where the array is empty. In this case this method can't return anything. So let's mark that return type as optional by throwing a ? at the end, allowing us to return nil for this edge case.

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    func removeRandomElement() -> Self.Element? {

    }
}

Secondly, because this method is going to modify the internal collection of items within this array, and because Array is a Struct type and not a Class, we need declare that our method is going to mutate the array value,

Structures and enumerations are value types [and by default] can’t be modified from within its instance methods... if you need to modify the properties ... within a particular method, you can opt in to mutating behavior for that method. The method can then mutate (that is, change) its properties from within the method, and any changes that it makes are written back to the original structure when the method ends.

You can opt in to this behavior by placing the mutating keyword before the func keyword for that method.

So, let's make sure our new method is marked as mutating accordingly,

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    mutating func removeRandomElement() -> Self.Element? {

    }
}

Ok. Now to actually make the method do the work. Let's handle that empty list edge case first. Do that while simultaneously grabbing our random index from the .indices property by using a guard statement,

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    mutating func removeRandomElement() -> Self.Element? {
        guard 
            let randomIndex = self.indices.randomElement()
        else {
            return nil
        }
    }
}

Now, if there are any conditions that cause .indices.randomElement() to return nil, such as the array being empty (and therefore it's .indices array also being empty), our method will simply give up and also return nil

The only thing left to do is to use our now guaranteed randomIndex value to remove the value at that index and return it,

// Extensions/Array+RemoveRandomElement.swift
extension Array {
    mutating func removeRandomElement() -> Self.Element? {
        guard 
            let randomIndex = self.indices.randomElement()
        else {
            return nil
        }
    }
    return self.remove(at:randomIndex)
}

Ok, we did it. Now anywhere we've got an Array value, we can grab random elements by simply calling .removeRandomElement(),

let randomTile: Tile? = tiles.removeRandomElement()

This "retroactively modeling" provided by Extensions feels really powerful and elegant. I've implemented a small handful of other extensions for other common within my game code. Maybe I'll share some other examples in a future post.

Speaking of that game! Interested in being an early tester? Send me a note using any of the links right down there 👇, and I'll add you to the list. I'm hoping to have an early preview of the MVP ready by the end of the month and I'd love to hear your feedback.