Swift 5.3 - Section 2: Collection Types

For this series of posts I have used extensively the book Swift Apprentice by Ray Wenderlich.
I strongly suggest this book, because if you buy the digital copy, Ray Wenderlich guarantees updates for the rest of the life!
I have also to say that Ray Wenderlich offers a cheat sheet available for download for free.
The purpose of these post is not to copy the book itself, that I strongly suggest to read, also doing the suggested exercises, that give the required confidence to really learn the language.
In particular I will not explain how to use Xcode or the playgrounds. The idea is that you buy the book and read it!
Final note: my background is deep experience in C#, so maybe some concepts are obvious for me. If you read this article, find it useful, but would like that I add something, don't hesitate to comment here below! But remember, my goal is not to copy the book :-)
Chapter 7: Arrays, Dictionaries & Sets
Arrays
Array literal
let evenNumbers = [2, 4, 6, 8]
var subscribers: [String] = []
let allZeros = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
Array properties and methods
var players = ["Alice", "Bob", "Cindy", "Dan"]
print(players.isEmpty)
// > false
if players.count < 2 {
print("We need at least two players!")
} else {
print("Let’s start!")
}
// > Let’s start!
var currentPlayer = players.first
print(currentPlayer as Any)
// > Optional("Alice")
print(players.last as Any)
// > Optional("Dan")
currentPlayer = players.min()
print(currentPlayer as Any)
// > Optional("Alice")
players.randomElement()
// > picks up a random element from the array (Optional)
Using countable ranges to make an ArraySlice
let upcomingPlayersSlice = players[1...2]
print(upcomingPlayersSlice[1], upcomingPlayersSlice[2])
// > "Bob Cindy\n"
To create a totally new array:
let upcomingPlayersArray = Array(players[1...2])
print(upcomingPlayersArray[0], upcomingPlayersArray[1])
// > "Bob Cindy\n"
Checking for an element
func isEliminated(player: String) -> Bool {
!players.contains(player)
}
It works also for ArraySlice:
players[1...3].contains("Bob") // true
Modifying arrays
Appending elements:
players.append("Eli")
players += ["Gina", "Tina", "Nina"]
Inserting elements:
players.insert("Frank", at: 5)
Removing elements:
var removedPlayer = players.removeLast()
print("\(removedPlayer) was removed")
// > Gina was removed
removedPlayer = players.remove(at: 2)
print("\(removedPlayer) was removed")
// > Cindy was removed
Updating elements:
print(players)
// > ["Alice", "Bob", "Dan", "Eli", "Frank"]
players[4] = "Franklin"
print(players)
// > ["Alice", "Bob", "Dan", "Eli", "Franklin"]
players[0...1] = ["Donna", "Craig", "Brian", "Anna"]
print(players)
// > ["Donna", "Craig", "Brian", "Anna", "Dan", "Eli", "Franklin"]
Moving elements:
players.swapAt(1, 3)
print(players)
// > ["Anna", "Brian", "Craig", "Donna", "Dan", "Eli", "Franklin"]
Sort:
players.sort()
print(players)
// > ["Anna", "Brian", "Craig", "Dan", "Donna", "Eli", "Franklin"]
Emptying:
players.removeAll()
print(players)
// > []
Iterating through an array
for i in 0..<players.count {
print(players[i])
}
// > Anna
// > Brian
// > Craig
// > Dan
// > Donna
// > Eli
// > Franklin
for player in players {
print(player)
}
// > Anna
// > Brian
// > Craig
// > Dan
// > Donna
// > Eli
// > Franklin
for (index, player) in players.enumerated() {
print("\(index + 1). \(player)")
}
// > 1. Anna
// > 2. Brian
// > 3. Craig
// > 4. Dan
// > 5. Donna
// > 6. Eli
// > 7. Franklin
Chopping arrays
DropFirst takes an array in input and returns the array minus 1 or more elements at the beginning:
var prices = [1.5, 10, 4.99, 2.30, 8.19]
let removeFirst = prices.dropFirst()
// > [10, 4.99, 2.30, 8.19]
let removeFirstTwo = prices.dropFirst(2)
// > [4.99, 2.30, 8.19]
DropLast does the same at the end:
let removeLast = prices.dropLast()
// > removeLast = [1.5, 10, 4.99, 2.30]
let removeLastTwo = prices.dropLast(2)
// > removeLastTwo = [1.5, 10, 4.99]
To take the first or last elements:
let firstTwo = prices.prefix(2)
// > [1.5, 10]
let lastTwo = prices.suffix(2)
// > [2.30, 8.19]
To remove all elements, eventually with a condition:
prices.removeAll()
prices.removeAll() { $0 > 2 }
Dictionaries
var namesAndScores = ["Anna": 2, "Brian": 2, "Craig": 8, "Donna": 6]
print(namesAndScores)
// > ["Craig": 8, "Anna": 2, "Donna": 6, "Brian": 2]
How to empty a dictionary:
namesAndScores = [:]
How to declare a new empty dictionary:
var pairs: [String: Int] = [:]
Accessing values
print(namesAndScores["Anna"]!) // 2
namesAndScores["Greg"] // nil
Using properties and methods
namesAndScores.isEmpty // false
namesAndScores.count // 4
Modifying dictionaries
bobData.updateValue("CA", forKey: "state")
bobData["city"] = "San Francisco"
Removing pairs
let removedValue = bobData.removeValue(forKey: "state")
bobData["city"] = nil
Iterating through dictionaries
for (player, score) in namesAndScores {
print("\(player) - \(score)")
}
// > Craig - 8
// > Anna - 2
// > Donna - 6
// > Brian - 2
for player in namesAndScores.keys {
print("\(player), ", terminator: "") // no newline
}
print("") // print one final newline
// > Craig, Anna, Donna, Brian,
for score in nameAndScores.values {
print("\(score), ", terminator: "") // no newline
}
print("") // print one final newline
// > 8, 2, 6, 2,
Sets
Creating sets is done from arrays:
let setOne: Set<Int> = [1]
let someArray = [1, 2, 3, 1]
var explicitSet: Set<Int> = [1, 2, 3, 1]
var someSet = Set([1, 2, 3, 1])
print(someSet) // > [2, 3, 1] but the order is not defined
Accessing elements
print(someSet.contains(1))
// > true
print(someSet.contains(4))
// > false
Adding and removing elements
someSet.insert(5)
let removedElement = someSet.remove(1)
print(removedElement!)
// > 1
Comparing with other Sets
let someSet: Set<Int> = [1, 2, 5]
let otherSet: Set<Int> = [5, 7, 13]
let intersection = someSet.intersection(otherSet) // [5]
let difference = someSet.symmetricDifference(otherSet) // [1, 2, 7, 13]
let union = someSet.union(otherSet) // [1, 2, 5, 7, 13]
To modify the Set itself instead of creating a new Set:
let someSet: Set<Int> = [1, 2, 5]
let otherSet: Set<Int> = [5, 7, 13]
someSet.formIntersection(otherSet) // [5]
someSet.formSymmetricDifference(otherSet) // [1, 2, 7, 13]
someSet.formUnion(otherSet) // [1, 2, 5, 7, 13]
Chapter 8: Collection Iteration with Closures
Closure basics
A closure is a variable pointing to an anonymous function:
var multiplyClosure: (Int, Int) -> Int
var multiplyClosure = { (a: Int, b: Int) -> Int in
return a * b
}
let result = multiplyClosure(4, 2)
Remove the return keyboard:
multiplyClosure = { (a: Int, b: Int) -> Int in
a * b
}
Use Swift type inference:
multiplyClosure = { (a, b) in
a * b
}
Omit parameter list:
multiplyClosure = {
$0 * $1
}
If the closure is passed as last parameter of another method, you can move it outside of the function call -* trailing closure syntax*:
operateOnNumbers(4, 2) {
$0 + $1
}
Multiple trailing closures syntax
If you have a function that has multiple closures as inputs, you can still call it in a special shorthand way:
func sequenced(first: ()->Void, second: ()->Void) {
first()
second()
}
sequenced {
print("Hello, ", terminator: "")
} second: {
print("world.")
}
Closures with no return type
It's possible to define a closure that returns nothing, simply make it to return Void:
let voidClosure: () -> Void = {
print("Swift Apprentice is awesome!")
}
voidClosure()
Capturing from the enclosing scope
A closure captures the enclosing scope:
var counter = 0
let incrementCounter = {
counter += 1
}
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter() // at the end, counter==5
It can be very useful to capture the enclosing scope, for example:
func countingClosure() -> ()->Int {
var counter = 0
let incrementCounter: () -> Int = {
counter += 1
return counter
}
return incrementCounter
}
let counter1 = countingClosure()
let counter2 = countingClosure()
counter1() // 1
counter2() // 1
counter1() // 2
counter1() // 3
counter2() // 2
Custom sorting with closures
let names = ["ZZZZZZ", "BB", "A", "CCCC", "EEEEE"]
names.sorted()
// ["A", "BB", "CCCC", "EEEEE", "ZZZZZZ"]
By specifying a custom closure:
names.sorted {
$0.count > $1.count
}
// ["ZZZZZZ", "EEEEE", "CCCC", "BB", "A"]
Iterating over collections with closures
Foreach:
let values = [1, 2, 3, 4, 5, 6]
values.forEach {
print("\($0): \($0*$0)")
}
Filter:
var prices = [1.5, 10, 4.99, 2.30, 8.19]
let largePrices = prices.filter {
$0 > 5
}
First:
let largePrice = prices.first {
$0 > 5
}
Map:
let salePrices = prices.map {
$0 * 0.9
}
Compact map is used to filter nil values:
let numbers2 = userInput.compactMap {
Int($0)
}
Flat map is used to flatten multi-dimensional arrays.
var letters = [ ["A", "B"], ["C"], ["D", "E", "F"] ]
letters.flatMap() { $0 }
Reduce (it takes a starting value and the next value, produce a result, and passes this as starting value to the next iteration):
let sum = prices.reduce(0) {
$0 + $1
}
There is also the function reduce(into:_:) where you don't return anything from the closure, but you have only one array that is iterated, and so it is even faster.
Sort:
var names = [ "Zeus", "Poseidon", "Ares", "Demeter" ]
names.sort()
names.sort{ (a, b) -> Bool in
a > b
}
names.sort(by: >)
Sorted returns a new collection that is sorted, instead of sorting the current collection.
Lazy collections
The lazy collection is created on demand, only when it is needed:
func isPrime(_ number: Int) -> Bool { ... }
let primes = (1...).lazy
.filter { isPrime($0) }
.prefix(10)
primes.forEach { print($0) }
Chapter 9: Strings
String as collection of characters
let string = "Matt"
for char in string {
print(char)
}
let stringLength = string.count
But: count returns the number of grapheme clusters.
The correct way is to use *unicodeScarlars":
let cafeCombining = "cafe\u{0301}"
for codePoint in cafeCombining.unicodeScalars {
print(codePoint.value)
}
Indexing strings
let firstIndex = cafeCombining.startIndex
let firstChar = cafeCombining[firstIndex]
let lastIndex = cafeCombining.index(before: cafeCombining.endIndex)
let lastChar = cafeCombining[lastIndex]
let fourthIndex = cafeCombining.index(cafeCombining.startIndex, offsetBy: 3)
let fourthChar = cafeCombining[fourthIndex]
The character is made of multiple code points. How to access them:
fourthChar.unicodeScalars.count // 2
fourthChar.unicodeScalars.forEach {
codePoint in print(codePoint.value)
}
Equality with combining characters
In Swift, by default, consider the same even two strings made of different characters but with same canonicalization. For example:
let cafeNormal = "café"
let cafeCombining = "cafe\u{0301}"
let equal = cafeNormal == cafeCombining
// > true
Strings as bi-directional collections
let name = "Matt"
let backwardsName = name.reversed()
let secondCharIndex = backwardsName.index(backwardsName.startIndex, offsetBy: 1)
let secondChar = backwardsName[secondCharIndex] // "t"
let backwardsNameString = String(backwardsName)
The reversed string is of type ReservedCollection< String> and doesn't use any more memory than the original string. Creating a new string out of it will duplicate the required memory.
Raw strings
Raw strings allows to avoid any special character:
let raw1 = #"Raw "No Escaping" \(no interpolation!). Use all the \ you want!"#
// > Raw "No Escaping" \(no interpolation!). Use all the \ you want!
Still it's possible to use string interpolation with raw strings:
let can = "can do that too"
let raw3 = #"Yes we \#(can)!"#
print(raw3)
// > Yes we can do that too!
Substrings
let fullName = "Matt Galloway"
let spaceIndex = fullName.firstIndex(of: " ")!
let firstName = fullName[fullName.startIndex..<spaceIndex] // "Matt"
let firstName = fullName[..<spaceIndex] // "Matt"
let lastName = fullName[fullName.index(after: spaceIndex)...] // "Galloway"
let lastNameString = String(lastName)
The substring is of type Substring and doesn't use any more memory than the original string. Creating a new string out of it will increase the required memory.
Character properties
let singleCharacter: Character = "x"
singleCharacter.isASCII
let space: Character = " "
space.isWhitespace
let hexDigit: Character = "d"
hexDigit.isHexDigit
let thaiNine: Character = "๙"
thaiNine.wholeNumberValue // 9, beucase "๙" is "9" in Thai
Encoding
let characters = "+\u{00bd}\u{21e8}\u{1f643}"
for i in characters.utf8 {
print("\(i) : \(String(i, radix: 2))")
}
for i in characters.utf16 {
print("\(i) : \(String(i, radix: 2))")
}