Learning iOS and Swift. Day 6: Structs and classes
Abstract
Briefly discussing structs and classes, with key differences, property and method declaration, and property observers.
There are three types of “algebraic” data types in Swift: enumerations, structs, and classes. That’s one more than Rust, which does not have classes.
Classes vs. Structs
In Swift, structs and classes are very similar in functionality and behavior.
The key differences I have noticed by now is that classes can be subclassed (i. e. they support inheritance), and the way they are passed around in memory: structs are passed by value (like Copy
values in Rust), while class instances are passed by reference (so, passing pointers around).
This can be easily illustrated by implementing a simple counter. With a struct, it looks like this:
struct StructCounter {
private(set) var value: Int = 0
mutating func inc() {
self.value += 1
}
mutating func dec() {
self.value -= 1
}
}
Notice that within a struct, methods that modify the value of the struct’s properties must be annotated with mutating
.
var s1 = StructCounter()
s1.inc() // s1.value is now 1
var s2 = s1 // s1 gets copied
s2.inc()
print(s1.value) // 1
print(s2.value) // 2
By annotating the value
property with private(set)
, we prevent the struct from being modified from the outside of the struct. If we tried to reassign the value nevertheless, we would get a compiler error:
myCounter.value = 100
struct_with_methods.swift:21:11: error: cannot assign to property: 'value' setter is inaccessible
myCounter.value = 100
~~~~~~~~~~^~~~~
The same counter functionality, but written as a class:
class ClassCounter {
private(set) var value: Int = 0
func inc() {
self.value += 1
}
func dec() {
self.value -= 1
}
}
In class definitions, mutating methods must not be annotated with mutating
.
var c1 = ClassCounter()
c1.inc() // c1.value is now 1
var c2 = c1 // c2 and c1 now point to the same instance
c2.inc() // so when you call a method on c2, c1 gets mutated as well
print(c1.value) // 2
print(c2.value) // 2
Property observers
It is possible to execute some code after (or before) a property has been set on a struct or class instance. This is done using a feature called “property observers,” which are a nice alternative to setter methods. An example use case of property observers is in an Active Record-like data structure, representing a record in the database. Before validating and writing string input, we need to trim all surrounding whitespace from a string. This section is inspired by an example from the book Swift in depth by Tjeerd in ’t Veen.
import Foundation
struct User {
let id: String
var name: String {
didSet {
self.name = self.name.trimmingCharacters(in: .whitespaces)
}
}
init(name: String) {
defer { self.name = name }
self.id = UUID.init().uuidString.lowercased()
self.name = ""
}
}
This example defines a struct called User
with two properties: an immutable id
set in the initializer, and a mutable name
.
I also defined a didSet
closure that trims the name
after it has been set:
var name: String {
didSet {
self.name = self.name.trimmingCharacters(in: .whitespaces)
}
}
The way to trim whitespaces from a string in Swift is using:
string.trimmingCharacters(in: .whitespaces)
Note that this method is provided by Foundation. Another feature from the Foundation framework is the UUID struct, providing methods to generate and manipulate UUIDs.
In the initializer, I generate a random UUID to use as the record’s unique identifier.
In this example, I treat the UUID as a string, because the actual data representation of a UUID is not relevant to this example.
I initialize name
as an empty string to let the compiler know that the value will always be initialized in the initializer.
The actual initial value for name
is set in a defer
closure, which will be fired after the rest of the initialization code.
This is because the didSet
closure does not fire in the initializer.
init(name: String) {
defer { self.name = name }
self.id = UUID.init().uuidString.lowercased()
self.name = ""
}
Let’s try this out:
var newUser = User(name: " Test ")
print(newUser)
// User(id: "ba3d5e23-431c-404c-a371-8ed306f07900", name: "Test")
As expected, upon initialization, all surrounding whitespace in the name
is trimmed.
What happens when we set a new value?
newUser.name = " A changed value "
print(newUser)
// User(id: "ba3d5e23-431c-404c-a371-8ed306f07900", name: "A changed value")
Here, too, the value has been correctly trimmed.
Besides didSet
, you can also add a willSet
observer, but it seems like it cannot be used to manipulate the property value:
var name: String {
didSet {
self.name = self.name.trimmingCharacters(in: .whitespaces)
}
willSet {
print("About to change the value to \(newValue)")
}
}