Swift 5.3 - Section 2: Collection Types

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))")
}