100 Days of Swift – Day 87

Hacking with Swift – Learning Project 24

Setting up

Create a new playground and name it “Swift Strings”.

Yes, that’s it to get started, all the why and what are here and I don’t want to repeat other people’s words!

Strings are not arrays

In this very informative video we learn that, in Swift, strings are much more than just arrays of characters (something that I learned in my very first Computer Science course, CS50, by David J. Malan) so even if we can write a loop to print out each element of the string, we cannot subscript it.

If we try that we are greeted by an error and encouraged to read the dedicated documentation. You know me, I am going to do it and, because you are following me, I am going to create a separate playground with all what I learnt in that and share it with all of you here from my Dropbox. Just, wait for the end of this project and before the challenges.

Paul also explains that we can extract that letter with a code like this:

let letter = name[name.index(name.startIndex, offsetBy: 3)]

…which means: access the name string at an index that is counting from the first index and going on by 3. Clear right? 🙂

We could also just write an extension that adds this functionality to the String type but then Apple decided not to do so because looping over substrings in a string would create very slow and underperforming code. Remember: loops are a good thing but when you start nesting them one inside the other they get slow very quickly.

Working with strings in Swift

In this video we get our hands dirty with some interesting methods for manipulating strings.

Prefix & Suffix

The first ones are the .hasPrefix() and .hasSuffix() which let us check whether the calling string has the passed prefix or suffix. We are then shown a way to extend this functionality so that we can remove an existing prefix or suffix. This works by creating two separate methods that accept a String parameter and return a String, check that they indeed have a prefix / suffix (otherwise just return the calling string) and, if so, return the calling string with a .dropFirst(prefix.count) or a .dropLast(suffix.count) call. These methods remove a certain amount of letters from the either end of the string.

Capitalisation

We can get a string have each of its words capitalised by calling the .capitalized method on it. If we would like sentence-like capitalisation we should create an extension that checks the string is not empty (i.e., it has a first element) then create a composite string made of the uppercased first letter plus the rest of the string with a .dropFirst() letter call then return it.

In Swift individual letters in a string are not strings themselves but Characters. So, when we call the .uppercased() method, we are actually calling it on a Character.

Contains

We can check if a string contains another string by using the container.contains(string) call. If we have an array of strings and we want to check if a string contain any of the elements of said array we could write an extension that creates a dedicated method for looping over the array and calling the .contains method on each element.

It would be better, though, to use the .contains(where:) method. This method wants a closure being passed to it at its end so we can actually call it like this: arrayToSearch.contains(where: string.contains). The .contains(where:) method will call its closure once for every element in the calling array until it finds one that returns true, at which point it stops. In this case it will see if any of the items in the array is contained in the passed string. The syntax is quite obscure but I think I just need to see it some times in action. In Apple Documentation the predicate we are passing this method is described as

a closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match.

Formatting strings with NSAttributedString

When we want to do more with our strings than just editing plain text, we can resort on using the NSAttributedString class. This is defined as a string that has associated attributes (such as visual style, hyperlinks, or accessibility data) for portions of its text. Of course, this is a subclass of our almighty NSObject. Reading from Apple Documentation:

An NSAttributedString object manages character strings and associated sets of attributes (for example, font and kerning) that apply to individual characters or ranges of characters in the string. An association of characters and their attributes is called an attributed string. […]

An attributed string identifies attributes by name, using an NSDictionary object to store a value under the given name.

So, we created a test string and an attributes [NSAttributesString.Key: Any] dictionary inside which we set the .foregroundColor to be white, the backgroundColor to be red and the .font to be a bold system font of size 36. Creating then a new constant with NSAttributedString(string: string, attributes: attributes) we got this in return:

Pretty cool, right?!
Pretty cool, right?!

We then performed another test creating a new mutable attributed string from our test string and then calling the .addAttribute method on it with different font sizes and ranges. Look at the result:

Even cooler!
Even cooler!

I got a bit confused here at first because I didn’t understand the range part. The location parameter of the NSRange initialiser describes the position in the string (so 0, like an array, is the first position), while the length parameter would include the elements 0, 1, 2, 3. So, why we begin from 5 afterwards? In this precise test Paul decided not to influence the space size which remains at font 12 I guess (which is the default value). For my curiosity, I will try to extend the font size change to the space after each word and here is the result:

Sure … it’s not beautiful but it is more coherent I guess!

Paul also invites us to browse other options for attributed strings, which are quite well documented in the Documentation.

Before moving on I wanted to spend a few words on NSMutableAttributedString. This is a class that declares additional methods for mutating the content of an attributed string. It is responsible for changing characters (with the replaceCharacters(in:with:) and the deleteCharacters(in:) methods), changing attributes like font traits, alignment and much more.

Swift Strings

As promised at the beginning of this article I am now going to read through the Apple Documentation page for Strings and jot down some notes about what I find most interesting.

First of all, Strings are defined as a Unicode string value that is a collection of characters and the data type itself is a struct. As such, they have value semantics, that is, modifying a copy of a string leaves the original unaffected.

In some parts there is written “locale settings” but I have no idea of what this means, maybe something with Unicode?

Accessing String Elements

Strings are presented as a collection of extended grapheme clusters, which approximate human-readable characters. The following example retrieves the first word of a longer string:

let name = "Marie Curie"
let firstSpace = name.firstIndex(of: " ") ?? name.endIndex
let firstName = name[..<firstSpace]

This use of array’s indexing is new to me but it is quite easy to understand: using the half-open range operator we ask for every index up to and excluding the firstSpace index. Brilliant!

To demonstrate this I tried to create an extension:

extension String {
    func extractFirstWord() -> String {
        guard !self.isEmpty else { return "" }
        
        let firstSpace = self.firstIndex(of: " ") ?? self.endIndex
        let firstWord = self[..<firstSpace]
        
        return String(firstWord)
    }
}

let longSentence = "Gas companies are screwing us all over the world!"
let firstWord = longSentence.extractFirstWord()

The example is dedicated with love to the gas company who just charged me some extra 270smc of natural gas consumption on assumption that I would have consumed them in the following period …

Accessing a String’s Unicode Representation

I am by now mean an Unicode connoisseur but I will show this example anyway. If we take this string as example:

let cafe = "Cafe\u{301} du 🌍"

Printing it will give “Café du 🌍”, printing cafe.count will print 9 and printing Array(cafe) will print “[”C”, “a”, “f”, “é”, ” “, “d”, “u”, ” “, “🌍”]”.

Reading on we discover that a string’s unicodeScalars property is a collection of Unicode scalar values, the 21-bit codes that are the basic unit of Unicode. Each scalar value is represented by a Unicode.Scalar instance and is equivalent to a UTF-32 code unit. Quite naturally Apple Documentation doesn’t explain anything more about Unicode and I will make a note to go delve into that side of things once these 100 days are over.

Anyway, printing cafe.unicodeScalars.count gives 10 as a result, not 9! Printing now the detailed array inside that will give us this “ [“C”, “a”, “f”, “e”, “\u{0301}“, ” “, “d”, “u”, ” “, “\u{0001F30D}”]” so we can see that while the emoji was quite obviously something different from the rest, the accented ‘e’ is made up of the regular e plus the accent character. Now for a bit of functional magic: look at what happens if I call print(cafe.unicodeScalars.map { $0.value }): [67, 97, 102, 101, 769, 32, 100, 117, 32, 127757] .

If we want to switch to what’s called UTF-16 View we discover that a string’s utf16 property is a collection of UTF-16 code units, the 16-bit encoding form of the Unicode scalar values. Each code unit is stored as an UInt16 (unsigned 16-bit integer) instance. If we print cafe.utf16.count we get back 11 because the world emoji is split up into two elements! Printing them out gives the same first 10 digits as before but gives 55356 and 57101 instead of 122757. This kind of encoding is the one used but the NSString API, the String technology of Objective-C.

We could go even further and look at the UTF-8 encoding, which comes from the C programming language, but I think you got the story by now.

Measuring the Length of a String

You may still recall what Paul said about counting the elements of a String. The Documentation reiterates on this by saying

When you need to know the length of a string, you must first consider what you’ll use the length for.

The main questions we have to ask ourselves are:

  • are we measuring the number of characters that will be displayed on the screen?
  • are we measuring the amount of storage needed for the string in a particular encoding?

Once this is clear we can look at some examples. To start, the ASCII character for the capital A is represented in the same way in each of its four views (Swift count, unicode scalars count, utf16 count and utf8 count) because its Unicode scalar value is 65, small enough for all of them.

Conversely, the flag of Puerto Rico is constructive from two Unicode scalar values, more precisely “\u{1F1F5}” and “\u{1F1F7}”. Each one of these is too big for UTF-16 and UTF-8 code units so, calling count in each of the views will, respectively, give back 1, 2, 4, 8!

We are once more reminded that it would be better to use isEmpty to check for a string’s lack of elements instead of comparing its length to 0.

That’s it, let’s move on to the review.

Review for Project 24

Here is what we learnt today:

  1. Swift’s strings are able to handle emoji and other complex characters. This allows us to use any language without worrying about whether it’s going to work.
  2. The hasPrefix() method checks whether a string starts with a substring. It will return if the string is empty or the prefix doesn’t match.
  3. Swift lets us convert strings to numbers and back. Going from a number to a string always works, but going the other way uses fail-able initialisers. Let’s go back and review what mailable initialisers are because I really do not remember. Basically they are there for when we work with optional values as the value could be there or could also not be there. We write them using the init?() syntax and should return nil if something goes wrong.
  4. Attributed strings can contain fonts, colours, underlines, strikethrough, URLs, and more. This lets us format them however we want.
  5. Reading a specific character from a string means starting at the start index and counting forwards until we find the letter we want. This is a slow operation, particularly if you want to read through a very large string.
  6. Using an explicit type with our NSAttributedString attributes dictionary helps us write less code. This means we can write shorthand names for our keys, such as .font, rather than writing out the full name each time.
  7. Single-line Swift strings start and end with double quotes. In comparison, multi-line strings start and end with three double quotes.
  8. Many UIKit views have built-in supported for attributed strings. This includes UITextField, UITextView, UILabel, and more.
  9. An NSRange stores both a location and a length. This type is useful when working with older Objective-C APIs, such as NSAttributedString and UITextChecker.
  10. By default, Swift can’t read individual letters in a string using someString[3]. Although we can add this functionality with an extension, it must be done carefully.
  11. NSAttributedString and NSMutableAttributedString both hold strings with formatting. The only difference is that mutable attributed strings can be modified after we create them.
  12. We can loop over strings like arrays. Both arrays and strings can be treated like sequences, which means we can loop over them.

Good! Let’s now move on to challenges!

Challenges

Challenge 1: create a String extension that adds a withPrefix() method. If the string already contains the prefix it should return itself; if it doesn’t contain the prefix, it should return itself with the prefix added. For example: “pet”.withPrefix(“car”) should return “carpet”.

This was an easy one but, for some reasons, it took me more time than expected to work it out. Here it is:

extension String {
    func addPrefix(_ prefix: String) -> String {
        guard !self.hasPrefix(prefix) else { return self }
        
        return prefix + self
    }
}

let pet = "pet" // "pet"
let carpet = pet.addPrefix("car") // "carpet"

Done! On to the next one!

Challenge 2: create a String extension that adds an isNumeric property that returns true if the string holds any sort of number. Tip: creating a Double from a String is a failable initialiser.

Well … I tried this and … it seems to work … just nothing in my code follows Paul’s suggestion… Look at this:

extension String {
    var isNumeric: Bool {
        guard !self.isEmpty else { return false }
        
        if Int(self) != nil || Double(self) != nil {
            return true
        } else {
            return false
        }
    }
}

let five = "5"
five.isNumeric // true
let fiveInLetters = "five"
fiveInLetters.isNumeric // false
let doubleFive = "5.0"
doubleFive.isNumeric // true
let doubleFiveInLetters = "five dot 0"
doubleFiveInLetters.isNumeric // false

It really seems to work but … well, let’s move to the next one and see what happens. Maybe I will get some kind of illumination while working at it.

Five minutes later …

Actually … just writing return Double(self) != nil covers everything!

Impressive…

Challenge 3: create a String extension that adds a lines property that returns an array of all the lines in a string. So, "this\nis\na\ntest" should return an array with four elements.

Well again … I think I have made it…!

extension String {
    var lines: [String] {
        guard !self.isEmpty else { return [""] }
        
        return self.components(separatedBy: "\n")
    }
}

let testString = "this\nis\na\ntest"
print(testString.lines) // ["this", "is", "a", "test"]

Incredible how something we learnt in Project 5 is coming back now!

So… I think I will call it a day and get a much deserved rest for today!


Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page. We are learning so much thanks to him and he deserves to know of our gratitude.

He has about 20 great books on Swift, all of which you can check about here.

The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: