Subscripting String Shortcuts

The use of subscripts in Swift has added a few nice features to its predecessor in Obj-C, not only for reading, but also for writing your own implementations.

If you are not familiar with subscripts, they define the logic that allows you to write shortcut accessors and setters for objects that can be understood as a collection, and are most commonly used in Arrays and Dictionaries:


var array = ["Hello", " ", "World", "!"]
array[2] //subscript returns "World"
array[2] = "Everybody" //The word "World" is now replaced with "Everybody" using a subscript

In swift, a new accessor option for fetching subcomponents of an object using a Range has been added. What’s nice about it is that you can specify if you want the last index to be included or not in your fetched set, like so:


var array = ["Hello", " ", "World", "!"]
array[0...2] //returns ["Hello", " ", "World"] //includes last index
array[0..<2] //returns ["Hello", " "] //excludes last index

for object in array[0...2] {
  //do something...
}

It’s a great thing that this logic also applies to Strings, since they can be understood as an ordered collection of Character objects. Unfortunately, the default framework subscript does not allow the input of Range objects (like Array objects do), which means that if you try to write something like this you will get an unfortunate compiler error:


var string = "Hello World!"
string[6] //throws a compiler error

The reason for this is that Strings are subscripted to use a more specialised notation of Range objects, which is used to denote the location of a character in a string, including composed character sequence and UTF-16 characters. A very interesting article on the importance of this has been written by Ole Begemann and published by objc.io, which you should read if you want to know more about the details of what this involves. If you wanted to play ball with the default framework libraries to get the substring between characters 2 and 7, you would have to write a subscript usage as follows:


var string = "Hello World!"
let indexStart = advance(string.startIndex, 2)
let indexEnd = advance(string.startIndex, 7)
string[indexStart...indexEnd] //does not throw a compiler error

As you can see, this has the potential to become an absolute pain in the a** and could get completely out of control if we want to use string subscripts often on a large scale project. Fortunately we can leverage categories/extensions to write faster subscript accessors that allow for the use of Integers. In our case, we would want to consider the following two cases for subscripts:

Indexes

First we need to write a subscript for getting and setting indexes, so that in our previous example, calling string[5] gives us the character “W” and calling string[5] = “?” will replace the “W” character with “?”. This is easily achieved with the following code:


subscript(index: Int) -> String {
  get {
    let index = advance(self.startIndex, index)
    return String(self[index])
  }
  set(newValue) {
    let index = advance(self.startIndex, index)
    let range = index...index
    self = stringByReplacingCharactersInRange(range, withString: newValue)
  }
}

Notice that all we did was wrap our previous code logic into specific implementations on each subscript sub-case. Effectively, all we do is convert an Int index into a String.Index index and call the default framework subscript logic for the getter. In the case of the setter, note how we are returning a String object instead of a Character object for our subscript. This is to allow us for greater flexibility when setting the value of an index. By doing this, we have overridden the base functionality to accept replacing a given character not only with another character, but also with a chain of them, allowing us to pull off some awesome tricks, like these:


var string = "Hello World!"
string[6] //returns "W"
string[6] = "?" //string is changed to "Hello ?orld"
string[6] //returns "?"
string[6] = "Super W" //string is changed to "Hello Super World!"
string[6] //returns "S";

Powerful stuff, huh? NEXT!

Ranges

Ranges work with exactly the same logic, but subtly different in its application. For the purpose of avoiding code duplication a private utility function has been added to our extension that can convert the Range value of a String into its Range equivalent. Again, we leverage the default implementation for the getter and add a fully custom one for the setter.


subscript(range: Range) -> String {
  get {
    return self[stringRangeForIntRange(range)]
  }
  set(newValue) {
    self =stringByReplacingCharactersInRange(stringRangeForIntRange(range),withString:newValue)
  }
}

privatefuncstringRangeForIntRange(range:Range) -> Range {
  letindexStart = advance(self.startIndex, range.startIndex)
  letindexEnd = advance(self.startIndex, range.endIndex)
  returnindexStart..

That's it! Now you can use fast string accessorgetters and setters anywhere in your code, go berserk and try stuff out like this:


var string = "HelloWorld!"
var emojiString = "emoji test: 🌑🌒🌓🌔🌕🌖🌗🌘"

string[1] //returns "e"
emojiString[12] //returns "🌑"

string[1...4] //returns "ello"
string[1..<4] //returns "ell"

emojiString[12...16] //returns "🌑🌒🌓🌔🌕"
emojiString[12..<16] //returns "🌑🌒🌓🌔"

string[0] = "J" //changes string to "JelloWorld!"
string[0] = "OMG H" //changes string to "OMG HelloWorld!"

emojiString[12] = "?" //changes string to "emoji test: ?🌒🌓🌔🌕🌖🌗🌘"
emojiString[12...16] = "YES!" //changes string to "emoji test: YES!🌖🌗🌘"

As always, all the sample code from my tutorials is openly available on Github. Feel free to use it as needed!

Author: Danny Bravo

Director @ EPIC