Swift 5.3 - Section 4: Advanced Topics
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 18: Access Control and Code Organization
Introducing access control
private(set) var balance: Dollars
Modifiers available in Swift:
- private: accessible only to the defining type, plus its nested types, and extension on the type within the same source file;
- fileprivate: accessible only to the source file where it is defined (for example, to allow the creation of instances of one class only from another class);
- internal: accessible only within the module where it is defined (this is the default access level);
- public: accessible anywhere;
- open: public, plus it's possible to override it in anther module.
Organizing code into extensions
Extension by behavior: adding extension to a class to add functionalities.
It's possible to declare an extension as private. This means that automatically all members of the extension are private by default.
Extensions by protocol conformance
Another technique is to organize extensions by protocol conformance:
extension CheckingAccount: CustomStringConvertible {
public var description: String {
"Checking Balance: $\(balance)"
}
}
available()
available() is a way to deprecate code before removing it completely:
@available(*, deprecated, message: "Use init(interestRate:pin:) instead")
@available(*, deprecated, message: "Use processInterest(pin:) instead")
The star denotes what platforms are affected by the deprecation: *, iOS, iOSMac, tvOS, watchOS.
Opaque return types
Let's suppose you have a function and want to return an instance of a type: you need to use the some keyword:
func createAccount() -> some Account {
CheckingAccount()
}
Testing
import XCTest
class BankingTests: XCTestCase {
func testSomething() {
}
}
To run the tests from the Playground:
BankingTests.defaultTestSuite.run()
XCTAssert
XCTAssert functions are used in tests to assert that certain conditions are met:
func testNewAccountBalanceZero() {
let checkingAccount = CheckingAccount()
XCTAssertEqual(checkingAccount.balance, 0)
}
func testCheckOverBudgetFails() {
let checkingAccount = CheckingAccount()
let check = checkingAccount.writeCheck(amount: 100)
XCTAssertNil(check)
}
XCFail and XCTSkip
XCFail is used to fail the test if some preconditions are not met, for example running the test on an old version of the simulator:
func testNewAPI() {
guard #available(iOS 14, *) else {
XCFail("Only available on iOS 14 and above")
return
}
// perform test
}
In alternative to fail it, you can use XCTSkip to skip the test:
func testNewAPI() {
guard #available(iOS 14, *) else {
throw XCTSkip("Only available on iOS 14 and above")
return
}
// perform test
}
Making things @testable
To make your internal interface visible to tests (private members remain private):
@testable import Banking
The setUp and tearDown methods
The setUp() method is executed before each test and the tearDown() method is executed after each test:
var checkingAccount: CheckingAccount!
override func setUp() {
super.setUp()
checkingAccount = CheckingAccount()
}
override func tearDown() {
checkingAccount.withdraw(amount: checkingAccount.balance)
super.tearDown()
}
Chapter 19: Custom Operators, Subscripts & Keypaths
Types of operators
- Unary: ! (prefix), as (postfix);
- *Binary": +, -, *, /, %, ==, !=, <, >, <=, >=, &&, ||
- Ternary: ? :
Your own operator
It's possible to define custom operators and use any character combinations:
infix operator **
func **(base: Int, power: Int) -> Int {
precondition(power >= 2)
var result = base
for _ in 2...power {
result *= base
}
return result
}
let base = 2
let exponent = 2
let result = base ** exponent
Compound assignment operator
Most built-in operators have a corresponding compound assignment operator:
infix operator **=
func **=(lhs: inout Int, rhs: Int) {
lhs = lhs ** rhs
}
var number = 2
number **= exponent
Generic operators
To allow the operator to work with all kind of integer types:
func **<T: BinaryInteger>(base: T, power: Int) -> T {
precondition(power >= 2)
var result = base
for _ in 2...power {
result *= base
}
return result
}
func **=<T: BinaryInteger>(lhs: inout T, rhs: Int) {
lhs = lhs ** rhs
}
let unsignedBase: UInt = 2
let unsignedResult = unsignedBase ** exponent
let base8: Int8 = 2
let result8 = base8 ** exponent
let unsignedBase8: UInt8 = 2
let unsignedResult8 = unsignedBase8 ** exponent
let base16: Int16 = 2
let result16 = base16 ** exponent
let unsignedBase16: UInt16 = 2
let unsignedResult16 = unsignedBase16 ** exponent
let base32: Int32 = 2
let result32 = base32 ** exponent
let unsignedBase32: UInt32 = 2
let unsignedResult32 = unsignedBase32 ** exponent
let base64: Int64 = 2
let result64 = base64 ** exponent
let unsignedBase64: UInt64 = 2
let unsignedResult64 = unsignedBase64 ** exponent
Precedence and associativity
If you want to use the new operator together with other operators, you need to provide information about precedence and associativity:
precedencegroup ExponentiationPrecedence {
associativity: right
higherThan: MultiplicationPrecedence
}
infix operator **: ExponentiationPrecedence
So now you can use it, without the need of parenthesis, in:
2 * 2 ** 3 ** 2
You could also have chosen associativity: none and forced to use parenthesis.
Subscripts
How to overload the [] operator:
subscript(parameterList) -> ReturnType {
get {
// return someValue of ReturnType
}
set(newValue) {
// set someValue of ReturnType to newValue
}
}
For example:
class Person {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
extension Person {
subscript(key: String) -> String? {
switch key {
case "name": return name
case "age": return "\(age)"
default: return nil
}
}
}
let me = Person(name: "Cosmin", age: 33)
me["name"]
me["age"]
me["gender"]
Subscripts parameters
You can add external parameter names to subscripts:
subscript(property key: String) -> String? {
// original code
}
me[property: "name"]
me[property: "age"]
me[property: "gender"]
Static subscripts
class File {
let name: String
init(name: String) {
self.name = name
}
static subscript(key: String) -> String {
switch key {
case "path": return "custom path"
default: return "default path"
}
}
}
File["path"]
File["PATH"]
Dynamic member lookup
@dynamicMemberLookup
class Instrument {
let brand: String
let year: Int
private let details: [String: String]
init(brand: String, year: Int, details: [String: String]) {
self.brand = brand
self.year = year
self.details = details
}
subscript(dynamicMember key: String) -> String {
switch key {
case "info": return "\(brand) made in \(year)."
default: return details[key] ?? ""
}
}
}
let instrument = Instrument(brand: "Roland", year: 2019, details: ["type": "acoustic", "pitch": "C"])
instrument.info
instrument.pitch
A derived class will inherit dynamic member lookup automatically:
class Guitar: Instrument {}
let guitar = Guitar(brand: "Fender", year: 2019, details: ["type": "electric", "pitch": "C"])
guitar.info
It's also possible to use dynamic member lookup for static subscripts:
@dynamicMemberLookup
class Folder {
let name: String
init(name: String) {
self.name = name
}
class subscript(dynamicMember key: String) -> String {
switch key {
case "path": return "custom path"
default: return "default path"
}
}
}
Folder.path
Folder.PATH
Keypaths
Keypaths are reference to properties, done via "":
class Tutorial {
let title: String
let author: Person
let details: (type: String, category: String)
init(title: String, author: Person, details: (type: String, category: String)) {
self.title = title
self.author = author
self.details = details
}
}
let tutorial = Tutorial(title: "Object Oriented Programming in Swift", author: me, details: (type: "Swift", category: "iOS"))
let title = \Tutorial.title
let tutorialTitle = tutorial[keyPath: title]
Keypaths can also go more levels down:
let authorName = \Tutorial.author.name
var tutorialAuthor = tutorial[keyPath: authorName]
How to use keypaths with tuples:
let type = \Tutorial.details.type
let tutorialType = tutorial[keyPath: type]
let category = \Tutorial.details.category
let tutorialCategory = tutorial[keyPath: category]
Appending keypaths
let authorPath = \Tutorial.author
let authorNamePath = authorPath.appending(path: \.name)
tutorialAuthor = tutorial[keyPath: authorNamePath]
Setting properties with keypaths
class Jukebox {
var song: String
init(song: String) {
self.song = song
}
}
let jukebox = Jukebox(song: "Nothing Else Matters")
let song = \Jukebox.song
jukebox[keyPath: song] = "Stairway to Heaven"
Keypath member lookup
It's possible to combine dynamic member lookups with keypaths:
struct Point {
let x, y: Int
}
@dynamicMemberLookup
struct Circle {
let center: Point
let radius: Int
subscript(dynamicMember keyPath: KeyPath<Point, Int>) -> Int {
center[keyPath: keyPath]
}
}
let center = Point(x: 1, y: 2)
let circle = Circle(center: center, radius: 1)
circle.x
circle.y
Chapter 20: Pattern Matching
If and guard
If with pattern matching:
func process(point: (x: Int, y: Int, z: Int)) -> String {
if case (0, 0, 0) = point {
return "At origin"
}
return "Not at origin"
}
let point = (x: 0, y: 0, z: 0)
let status = process(point: point) // At origin
Guard with pattern matching:
func process(point: (x: Int, y: Int, z: Int)) -> String {
guard case (0, 0, 0) = point else {
return "Not at origin"
}
// guaranteed point is at the origin
return "At origin"
}
Switch
func process(point: (x: Int, y: Int, z: Int)) -> String {
let closeRange = -2...2
let midRange = -5...5
switch point {
case (0, 0, 0):
return "At origin"
case (closeRange, closeRange, closeRange):
return "Very close to origin"
case (midRange, midRange, midRange):
return "Nearby origin"
default:
return "Not near origin"
}
}
let point = (x: 15, y: 5, z: 3)
let status = process(point: point) // Not near origin
for
Pattern matching can be used as a filter in a for loop:
let groupSizes = [1, 5, 4, 6, 2, 1, 3]
for case 1 in groupSizes {
print("Found an individual") // 2 times
}
Wildcard pattern
if case (_, 0, 0) = coordinate {
// x can be any value. y and z must be exactly 0.
print("On the x-axis") // Printed!
}
Value-binding pattern
You use var or let to declare a variable or constant while doing pattern matching:
if case (let x, 0, 0) = coordinate {
print("On the x-axis at \(x)") // Printed: 1
}
To bind multiple values:
if case let (x, y, 0) = coordinate {
print("On the x-y plane at (\(x), \(y))") // Printed: 1, 0
}
Enumerate case pattern
enum Direction {
case north, south, east, west
}
let heading = Direction.north
if case .north = heading {
print("Don’t forget your jacket") // Printed!
}
Enumeration combined with value binding pattern:
enum Organism {
case plant
case animal(legs: Int)
}
let pet = Organism.animal(legs: 4)
switch pet {
case .animal(let legs):
print("Potentially cuddly with \(legs) legs") // Printed: 4
default: print("No chance for cuddles")
}
Optional pattern
Check for non-null values:
let names: [String?] = ["Michelle", nil, "Brandon", "Christine", nil, "David"]
for case let name? in names {
print(name) // 4 times
}
"Is" type-casting pattern
Is is used to check if a value is of a particular type:
let response: [Any] = [15, "George", 2.0]
for element in response {
switch element {
case is String:
print("Found a string") // 1 time
default:
print("Found something else") // 2 times
}
}
"As" type-casting pattern
for element in response {
switch element {
case let text as String:
print("Found a string: \(text)") // 1 time
default:
print("Found something else") // 2 times
}
}
Qualifying with where
You can add a where clause to pattern matching:
for number in 1...9 {
switch number {
case let x where x % 2 == 0:
print("even") // 4 times
default:
print("odd") // 5 times
}
}
Chaining with commas
It's possible to chain multiple patterns in a single case:
func timeOfDayDescription(hour: Int) -> String {
switch hour {
case 0, 1, 2, 3, 4, 5:
return "Early morning"
case 6, 7, 8, 9, 10, 11:
return "Morning"
case 12, 13, 14, 15, 16:
return "Afternoon"
case 17, 18, 19:
return "Evening"
case 20, 21, 22, 23:
return "Late evening"
default:
return "INVALID HOUR!"
}
}
let timeOfDay = timeOfDayDescription(hour: 12) // Afternoon
Sample filtering with bind value:
if case .animal(let legs) = pet, case 2...4 = legs {
print("potentially cuddly") // Printed!
} else {
print("no chance for cuddles")
}
Another sample: what follows
enum Number {
case integerValue(Int)
case doubleValue(Double)
case booleanValue(Bool)
}
let a = 5
let b = 6
let c: Number? = .integerValue(7)
let d: Number? = .integerValue(8)
if a != b {
if let c = c {
if let d = d {
if case .integerValue(let cValue) = c {
if case .integerValue(let dValue) = d {
if dValue > cValue {
print("a and b are different") // Printed!
print("d is greater than c") // Printed!
print("sum: \(a + b + cValue + dValue)") // 26
}
}
}
}
}
}
can be shortened to:
if a != b,
let c = c,
let d = d,
case .integerValue(let cValue) = c,
case .integerValue(let dValue) = d,
dValue > cValue {
print("a and b are different") // Printed!
print("d is greater than c") // Printed!
print("sum: \(a + b + cValue + dValue)") // Printed: 26
}
Custom tuple
It's possible to create a tuple on the fly to match it:
let name = "Bob"
let age = 23
if case ("Bob", 23) = (name, age) {
print("Found the right Bob!") // Printed!
}
Another sample:
var username: String?
var password: String?
switch (username, password) {
case let (username?, password?):
print("Success! User: \(username) Pass: \(password)")
case let (username?, nil):
print("Password is missing. User: \(username)")
case let (nil, password?):
print("Username is missing. Pass: \(password)")
case (nil, nil):
print("Both username and password are missing") // Printed!
}
Fun with wildcards
This is the basic sample:
for _ in 1...3 {
print("hi") // 3 times
}
Verify that an optional exists:
let user: String? = "Bob"
guard let _ = user else {
print("There is no user.")
fatalError()
}
print("User exists, but identity not needed.") // Printed!
But the best way to validate against an optional is still:
guard user != nil else {
print("There is no user.")
fatalError()
}
Final sample:
struct Rectangle {
let width: Int
let height: Int
let background: String
}
let view = Rectangle(width: 15, height: 60, background: "Green")
switch view {
case _ where view.height < 50:
print("Shorter than 50 units")
case _ where view.width > 20:
print("Over 50 tall, & over 20 wide")
case _ where view.background == "Green":
print("Over 50 tall, at most 20 wide, & green") // Printed!
default:
print("This view can’t be described by this example")
}
Overloading ~=
It's possible to overload the ~= operator to provide your own pattern matching:
func ~=(pattern: [Int], value: Int) -> Bool {
for i in pattern {
if i == value {
return true
}
}
return false
}
And now you can use it in this way:
let list = [0, 1, 2, 3]
let integer = 2
let isInArray = (list ~= integer) // true
if case list = integer {
print("The integer is in the array") // Printed!
} else {
print("The integer is not in the array")
}
Chapter 21: Error Handling
Failable initializers
If you try to initialize an integer or an enum, it could fail, and return nil.
let value = Int("3") // Optional(3)
let failedValue = Int("nope") // nil
enum PetFood: String {
case kibble, canned
}
let morning = PetFood(rawValue: "kibble") // Optional(.kibble)
let snack = PetFood(rawValue: "fuuud!") // nil
You can write your own failable initializers:
struct PetHouse {
let squareFeet: Int
init?(squareFeet: Int) {
if squareFeet < 1 {
return nil
}
self.squareFeet = squareFeet
}
}
let tooSmall = PetHouse(squareFeet: 0) // nil
let house = PetHouse(squareFeet: 1) // Optional(Pethouse)
Optional chaining
In case you have a deep structure with nested optional, you can check them with optional chaining:
if let sound = janie.pet?.favoriteToy?.sound {
print("Sound \(sound).")
} else {
print("No sound.")
}
Map and compactMap
In case you have a vector, you can project it to properties that can be nil:
let team = [janie, tammy, felipe]
let petNames = team.map { $0.pet?.name }
// > Optional("Delia")
// > Optional("Evil Cat Overlord")
// > nil
To skip the nil, you can use compactMap:
let betterPetNames = team.compactMap { $0.pet?.name }
// > Delia
// > Evil Cat Overlord
Note that skipping the nils, the results are not optionals anymore.
Error protocol
The Error protocol is like an exception. It suits well to make it an Enum:
class Pastry {
let flavor: String
var numberOnHand: Int
init(flavor: String, numberOnHand: Int) {
self.flavor = flavor
self.numberOnHand = numberOnHand
}
}
enum BakeryError: Error {
case tooFew(numberOnHand: Int)
case doNotSell
case wrongFlavor
}
Throwing errors
You raise errors throwing them:
class Bakery {
var itemsForSale = [
"Cookie": Pastry(flavor: "ChocolateChip", numberOnHand: 20),
"PopTart": Pastry(flavor: "WildBerry", numberOnHand: 13),
"Donut" : Pastry(flavor: "Sprinkles", numberOnHand: 24),
"HandPie": Pastry(flavor: "Cherry", numberOnHand: 6)
]
func open(_ now: Bool = Bool.random()) throws -> Bool {
guard now else {
throw Bool.random() ? BakeryError.inventory
: BakeryError.noPower
}
}
func orderPastry(item: String, amountRequested: Int, flavor: String) throws -> Int {
guard let pastry = itemsForSale[item] else {
throw BakeryError.doNotSell
}
guard flavor == pastry.flavor else {
throw BakeryError.wrongFlavor
}
guard amountRequested <= pastry.numberOnHand else {
throw BakeryError.tooFew(numberOnHand: pastry.numberOnHand)
}
pastry.numberOnHand -= amountRequested
return pastry.numberOnHand
}
}
Handling errors
You handle errors catching them:
do {
try bakery.open()
try bakery.orderPastry(item: "Albatross", amountRequested: 1, flavor: "AlbatrossFlavor")
} catch BakeryError.invetory, BakeryError.noPower {
print("Sorry, the bakery is now closed.")
} doNotSell {
print("Sorry, but we don’t sell this item.")
} catch BakeryError.wrongFlavor {
print("Sorry, but we don’t carry this flavor.")
} catch BakeryError.tooFew {
print("Sorry, we don’t have enough items to fulfill your order.")
}
Code that can throw errors must always be inside a do block.
Not looking at the detailed error
If you don't care about the error and you only want to check that there are no errors:
let open = try?.bakery.open(false)
let remaining = try? bakery.orderPastry(item: "Albatross", amountRequested: 1, flavor: "AlbatrossFlavor")
Stopping your program on an error
If want to check for any error, you can catch without anything specific:
do {
try bakery.open(true)
try bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "ChocolateChip")
} catch {
fatalError()
}
Or a shortcut:
try! bakery.open(true)
try! bakery.orderPastry(item: "Cookie", amountRequested: 1, flavor: "ChocolateChip")
Handling multiple errors
func moveSafely(_ movement: () throws -> ()) -> String {
do {
try movement()
return "Completed operation successfully."
} catch PugBotError.invalidMove(let found, let expected) {
return "The PugBot was supposed to move \(expected), but moved \(found) instead."
} catch PugbotError.endOfPath {
return "The PugBot tried to move past the end of the path."
} catch {
return "An unknown error occurred."
}
}
Rethrows
A function that takes a throwing closure parameter can choose to rethrow:
func perform(times: Int, movement: () throws -> ()) rethrows {
for _ in 1...times {
try movement()
}
}
GDC (Grand Central Dispatch)
It's possible to use GDC to create two types of work queue: serial and concurrent.
func log(message: String) {
let thread = Thread.current.isMainThread ? "Main" : "Background"
print("\(thread) thread: \(message).")
}
func addNumbers(upTo range: Int) -> Int {
log(message: "Adding numbers...")
return (1...range).reduce(0, +)
}
let queue = DispatchQueue(label: "queue")
func execute<Result>(backgroundWork: @escaping () -> Result, mainWork: @escaping (Result) -> ()) {
queue.async {
let result = backgroundWork()
DispatchQueue.main.async { mainWork(result) }
}
}
execute(backgroundWork: { addNumbers(upTo: 100) },
mainWork: { log(message: "The sum is \($0)") })
To capture errors thrown by the asynchronous functions, you need to use the Result type:
enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
}
Sample code to return this Result:
struct Tutorial {
let title: String
let author: String
}
enum TutorialError: Error {
case rejected
}
func feedback(for tutorial: Tutorial) -> Result<String, TutorialError> {
Bool.random() ? .success("published") : .failure(.rejected)
}
Sample code to call this function:
func edit(_ tutorial: Tutorial) {
queue.async {
let result = feedback(for: tutorial)
DispatchQueue.main.async {
switch result {
case let .success(data):
print("\(tutorial.title) by \(tutorial.author) was \(data) on the website.")
case let .failure(error):
print("\(tutorial.title) by \(tutorial.author) was \(error).")
}
}
}
}
let tutorial = Tutorial(title: "What’s new in Swift 5.1", author: "Cosmin Pupăză")
edit(tutorial)
How to use the Result in synchronous code:
let result = feedback(for: tutorial)
do {
let data = try result.get()
print("\(tutorial.title) by \(tutorial.author) was \(data) on the website.")
} catch {
print("\(tutorial.title) by \(tutorial.author) was \(error).")
}
Chapter 22: Encoding & Decoding Types
Encodable and Decodable protocols
Encodable protocol:
func encode(to: Encoder) throws
Decodable protocol:
init(from decoder: Decoder) throws
Codable is something both Encodable and Decodable:
typealias Codable = Encodable & Decodable
If you have a struct whose fields are all codable, then it's enough to make it to conform to Codable:
struct Employee: Codable {
var name: String
var id: Int
}
If your struct has some custom fields, it's enough to make them too conformant to Codable:
struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy?
}
struct Toy: Codable {
var name: String
}
All collection types, including array and dictionary, of Codable are also automatically Codable.
JSONEncoder and JSONDecoder
If you have custom types to be converted to JSON:
let toy1 = Toy(name: "Teddy Bear");
let employee1 = Employee(name: "John Appleseed", id: 7, favoriteToy: toy1)
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(employee1)
print(jsonData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString) // {"name":"John Appleseed","id":7,"favoriteToy":{"name":"Teddy Bear"}}
If you want to decode JSON back to an instance:
let jsonDecoder = JSONDecoder()
let employee2 = try jsonDecoder.decode(Employee.self, from: jsonData)
CodingKey protocol and CodingKey enum
The CodingKeys enum, which confirms to the CodingKey protocol, lets you rename specific properties when doing serialization:
struct Employee: Codable {
var name: String
var id: Int
var favoriteToy: Toy?
enum CodingKeys: String, CodingKey {
case id = "employeeId"
case name
case favoriteToy
}
}
// { "employeeId": 7, "name": "John Appleseed", "favoriteToy": {"name": "Teddy Bear"}}
The encode function
In case you need to write a custom encoder, you need to override the encode and decode functions:
enum CodingKeys: String, CodingKey {
case id = "employeeId"
case name
case gift
}
extension Employee: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encode(favoriteToy?.name, forKey: .gift)
}
}
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
id = try values.decode(Int.self, forKey: .id)
if let gift = try values.decode(String?.self, forKey: .gift) {
favoriteToy = Toy(name: gift)
}
}
}
>> {"name":"John Appleseed","gift":null,"employeeId":7}
encodeIfPresent and decodeIfPresent
encodeIfPresent and decodeIfPresent are used to skip null values:
extension Employee: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(id, forKey: .id)
try container.encodeIfPresent(favoriteToy?.name, forKey: .gift)
}
}
extension Employee: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
id = try values.decode(Int.self, forKey: .id)
if let gift = try values.decodeIfPresent(String.self, forKey: .gift) {
favoriteToy = Toy(name: gift)
}
}
}
Writing tests for the Encode and Decoder
To check that encoder and decoder are in sync:
import XCTest
class EncoderDecoderTests: XCTestCase {
var jsonEncoder: JSONEncoder!
var jsonDecoder: JSONDecoder!
var toy1: Toy!
var employee1: Employee!
override func setUp() {
super.setUp()
jsonEncoder = JSONEncoder()
jsonDecoder = JSONDecoder()
toy1 = Toy(name: "Teddy Bear")
employee1 = Employee(name: "John Appleseed", id: 7, favoriteToy: toy1)
}
func testEncoder() {
let jsonData = try? jsonEncoder.encode(employee1)
XCTAssertNotNil(jsonData, "Encoding failed")
let jsonString = String(data: jsonData!, encoding: .utf8)!
XCTAssertEqual(jsonString, "{\"name\":\"John Appleseed\", \"gift\":\"Teddy Bear\",\"employeeId\":7}")
}
func testDecoder() {
let jsonData = try! jsonEncoder.encode(employee1)
let employee2 = try? jsonDecoder.decode(Employee.self, from: jsonData)
XCTAssertNotNil(employee2)
XCTAssertEqual(employee1.name, employee2!.name)
XCTAssertEqual(employee1.id, employee2!.id)
XCTAssertEqual(employee1.favoriteToy?.name, employee2!.favoriteToy?.name)
}
}
EncoderDecoderTests.defaultTestSuite.run()
Chapter 23: Memory Management
Reference cycles for classes
A memory leak comes from two instances referencing each other.
Weak references
Weak references don't count on object references and automatically detect when the other instance is gone. Because of this, they always must be declared as Optional.
weak var editor: Editor?
Unowned references
Unowned references are like weak, i.e. they don't change the reference count, with the difference that they can never be null:
unowned let author: Author
Reference cycles for closures
Closures capture the context of the enclosing scope.
For example, adding this property:
lazy var description: () -> String = {
"\(self.title) by \(self.author.name)"
}
The property is lazy so evaluated during the first use, so you create a strong reference cycle between Self and the closure.
Capture lists
A capture lists allows to control how you want the closure to be bound to the objects it refers to.
counter = 0
f = { [c = counter] in print(c) }
counter = 1
f()
Shorthand to create a local variable counter shadowing the counter of the bound object:
counter = 0
f = { [counter] in print(counter) }
counter = 1
f()
Unowned self
It's possible to break the reference cycle with an unowned self using a capture list:
lazy var description: () -> String = {
[unowned self] in "\(self.title) by \(self.author.name)"
}
Weak self
If you can't capture Self because it can become nil, you can use a weak self.
For example, the following code breaks, because you deallocate tutorial and author at the end of the loop:
let tutorialDescription: () -> String
do {
let author = Author(name: "Cosmin")
let tutorial = Tutorial(title: "Memory management", author: author)
tutorialDescription = tutorial.description
}
print(tutorialDescription())
To fix this:
lazy var description: () -> String = {
[weak self] in "\(self?.title) by \(self?.author.name)"
}
Weak self won't extend the lifetime of self. If the underlying self goes away, it gets set to nil.
The strong-weak pattern
The strong-weak pattern converts the weak reference to a strong one:
lazy var description: () -> String = {
[weak self] in guard let self = self else {
return "The tutorial is no longer available."
}
return "\(self.title) by \(self.author.name)"
}
Chapter 24: Value Types & Value Semantics
Structs and enums are value types.
Classes and functions are reference types.
Arrays of value types implement value semantic.
Reference types defined as immutable behave like values types.
Defining value semantics
With value semantics, to change the value of a variable, you must hold a reference to that variable.
Copy-on-write on the rescue
How to implement copy-on-write:
struct PaintingPlan { // a value type, containing ...
// a value type
var accent = Color.white
// a private reference type, for "deep storage"
private var bucket = Bucket()
// a pseudo-value type, using the deep storage
var bucketColor: Color {
get {
bucket.color
}
set {
bucket = Bucket(color: newValue)
}
}
}
Optimization:
struct PaintingPlan { // a value type, containing ...
// ... as above ...
// a computed property facade over deep storage
// with copy-on-write and in-place mutation when possible
var bucketColor: Color {
get {
bucket.color
}
set {
if isKnownUniquelyReferenced(&bucket) {
bucket.color = bucketColor
} else {
bucket = Bucket(color: newValue)
}
}
}
}
Sidebar: property wrappers
As the copy-on-write pattern is verbose, you can use a property wrapper to make it more concise:
struct PaintingPlan {
var accent = Color.white
@CopyOnWriteColor var bucketColor = .blue
}
The CopyOnWriteColor is expanded by the compiler into:
private var _bucketColor = CopyOnWriteColor(wrappedValue: .blue)
var bucketColor: Color {
get { _bucketColor.wrappedValue }
set { _bucketColor.wrappedValue = newValue }
}
And here is the definition of CopyOnWriteColor:
@propertyWrapper
struct CopyOnWriteColor {
init(wrappedValue: Color) {
self.bucket = Bucket(color: wrappedValue)
}
private var bucket: Bucket
var wrappedValue: Color {
get {
bucket.color
}
set {
if isKnownUniquelyReferenced(&bucket) {
bucket.color = newValue
} else {
bucket = Bucket(color:newValue)
}
}
}
}
Chapter 25: Protocol-Oriented Programming
Introducing protocol extensions
Already seen how to implement a protocol via an extension:
protocol TeamRecord {
var wins: Int { get }
var losses: Int { get }
var winningPercentage: Double { get }
}
extension TeamRecord {
var gamesPlayed: Int {
wins + losses
}
}
The extension includes the implementation of the method.
Now it's possible to define a class confirming to the protocol, and also use the extension method:
struct BaseballRecord: TeamRecord {
var wins: Int
var losses: Int
var winningPercentage: Double {
Double(wins) / Double(wins + losses)
}
}
let sanFranciscoSwifts = BaseballRecord(wins: 10, losses: 5)
sanFranciscoSwifts.gamesPlayed // 15
Default implementations
To avoid the need of implement a method in every class and struct confirming to a protocol, you can define a default implementation in the protocol itself:
extension TeamRecord {
var winningPercentage: Double {
Double(wins) / Double(wins + losses)
}
}
Clearly a class or struct can reimplement this method if needed.
Understanding protocol extension dispatch
Note: if a type defines a method or property in a protocol extension, without declaring it in the protocol itself, dispatch is static, i.e. it considers the static type of the variable and not its actual instance type.
protocol WinLoss {
var wins: Int { get }
var losses: Int { get }
}
extension WinLoss {
var winningPercentage: Double {
Double(wins) / Double(wins + losses)
}
}
struct CricketRecord: WinLoss {
var wins: Int
var losses: Int
var draws: Int
var winningPercentage: Double {
Double(wins) / Double(wins + losses + draws)
}
}
let miamiTuples = CricketRecord(wins: 8, losses: 7, draws: 1)
let winLoss: WinLoss = miamiTuples
miamiTuples.winningPercentage // 0.5
winLoss.winningPercentage // 0.53 !!!
Type constraints
With type constraints on a protocol extension, you can call properties and methods from another type inside the extension itself:
protocol PostSeasonEligible {
var minimumWinsForPlayoffs: Int { get }
}
extension TeamRecord where Self: PostSeasonEligible {
var isPlayoffEligible: Bool {
wins > minimumWinsForPlayoffs
}
}
Note that the constraint could be another protocol itself.
Protocol-oriented benefits
- Program to interfaces, not implementations
In particular, you can program against types that don't support inheritance - Traits, mixins and multiple inheritance
Your types can also support multiple protocols, so have some sort of multiple inheritance - Simplicity
Use default implementations to keep code in one place
Why Swift is a protocol-oriented language
There aren't many classes in the Swift class library, because it uses many structs. For example, Array and Dictionary are structs.
They can be extended with protocols, that can apply to both Array and Dictionary. In an OOP language, Array and Dictionary should have been based on the same base class, while with Swift, this is not necessary.
Chapter 26: Advanced Protocols & Generics
Existential protocols
Existential type is a type implementing a protocol:
protocol Pet {
var name: String { get }
}
struct Cat: Pet {
var name: String
}
var somePet: Pet = Cat(name: "Whiskers")
Non-existential protocols
If a protocol has associated types, you can't use it as an existential type:
protocol Pet {
associatedtype Food
var name: String { get }
}
var somePet: Pet = Cat(name: "Whiskers")
// Now you get a compile time error
Constraining the protocol to a specific type
You can add constraints to associated types:
protocol WeightCalculatable {
associatedtype WeightType: Numeric
var weight: WeightType { get }
}
Expressing relationships between types
Sample of mapping protocols and concrete types with the factory pattern:
protocol Product {
init()
}
protocol ProductionLine {
associatedtype ProductType
func produce() -> ProductType
}
protocol Factory {
associatedtype ProductType
func produce() -> [ProductType]
}
So new we can define concrete types:
struct GenericProductionLine<P: Product>: ProductionLine {
func produce() -> P {
P()
}
}
struct GenericFactory<P: Product>: Factory {
var productionLines: [GenericProductionLine<P>] = []
func produce() -> [P] {
var newItems: [P] = []
productionLines.forEach { newItems.append($0.produce()) }
print("Finished Production")
print("-------------------")
return newItems
}
}
Final usage of the concrete types:
var carFactory = GenericFactory<Car>()
carFactory.productionLines = [GenericProductionLine<Car>(), GenericProductionLine<Car>()]
carFactory.produce()
Type erasure
Type erasure is a technique to erase type information that is not important:
let array = Array(1...10)
let set = Set(1...10)
let reversedArray = array.reversed()
let arrayCollection = [array, Array(set), Array(reversedArray) // [[Int]]
let collections = [AnyCollection(array),
AnyCollection(set),
AnyCollection(array.reversed())]
let total = collections.flatMap { $0 }.reduce(0, +) // 165
Opaque return types
Opaque return types allow not to create an Any*** wrapper type.
This feature allows to have a function that returns a protocol, for example:
func makeValue() -> some FixedWithInteger {
42
}
The function returns a compiler-known well defined type. For example it's not possible in one if branch to return Int(42) and in the else Int8(24): both cases must return always the same type.
It's also possible to return a composition of protocols:
func makeEquatableNumericInt() -> some Numeric & Equatable { 1 }
func makeEquatableNumericDouble() -> some Numeric & Equatable { 1.0 }
let value1 = makeEquatableNumericInt()
let value2 = makeEquatableNumericInt()
print(value1 == value2) // true
print(value1 + value2) // 2
print(value1 > value2) // error: it misses Comparable
Recursive protocols
A protocol can be recursive, for example:
protocol GraphNode {
var connectedNodes: [GraphNode] { get set }
}
If instead you want to define a Matrioska class, with constrained Self:
protocol Matryoshka: AnyObject {
var inside: Self? { get set }
}
final class HandCraftedMatryoshka: Matryoshka {
var inside: HandCraftedMatryoshka?
}
final class MachineCraftedMatryoshka: Matryoshka {
var inside: MachineCraftedMatryoshka?
}
Heterogeneous collections
If you want to have an array of different types:
protocol WeightCalculatable {
associatedtype WeightType: Numeric
var weight: WeightType { get }
}
var array1: [WeightCalculatable] = [] // compile error
var array2: [HeavyThing] = []
var array3: [LightThing] = []
Xcode will suggest to declare the array as [Any].
Type erasure
Using Any[] like before is not good, because too generic.
You can list all the types in the array declaration, for example:
class AnyHeavyThing<T: Numeric>: WeightCalculatable {
var weight: T {
123
}
}
class HeavyThing2: AnyHeavyThing<Int> {
override var weight: Int {
100
}
}
class VeryHeavyThing2: AnyHeavyThing<Int> {
override var weight: Int {
9001
}
}
var heavyList2 = [HeavyThing2(), VeryHeavyThing2()]
heavyList2.forEach { print($0.weight) }
Opaque return types
In case you want to hide details from the Factory example before:
func makeFactory() -> Factory { // compile error
GenericFactory<Car>()
}
let myFactory = makeFactory()
But this generates a compile time error. But it can be fixed:
func makeFactory() -> some Factory { // compiles!
GenericFactory<Car>()
}
Practically, the compiler knows that it's a GenericFactory< Car>, but it exposes it as a Factory protocol.
In fact, the following code doesn't compile because the compiler must know the type:
func makeFactory(isChocolate: Bool) -> some Factory {
if isChocolate {
return GenericFactory<Chocolate>()
} else {
return GenericFactory<Car>()
}
}