A Swift Tip: Extending Arrays To Do Whatever We Want
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.