In order to properly kick-start our journey through Swift's best practices and design patterns, I believe it's important that we take some time to go back to the basics. It's important to always keep your foundation strong; the more we advance through this book, the more we'll rely on those concepts.
I'll assume that you have a proper understanding of object-oriented programming (OPP) fundamentals, classes, inheritance, composition, and other techniques, as well as a fundamental understanding of the differences between value and reference types. If you're rusty on these concepts, you shouldn't worry too much, as we'll cover them shortly.
This chapter will dive deeply into the Swift language. What is a struct
, and what is a class
? What are their differences? Should you use an enum
or an OptionSet
? All of these questions will be answered in this chapter. We'll go back to the basics of classes and inheritance, and we'll discover the power of value types and immutability. We'll look it functions, closures, and currying. If you're unfamiliar with these constructs, or if you just want to get a refresher, you should tag along as we go back to the basics. These basics are essential to the Swift language, and are required to successfully apply efficient design patterns and best practices.
In this first chapter, we'll take the time to go back to the basics by covering the following topics:
- Classes and structs: what they are, and how they behave
- Exploring enums and their capabilities and extensibility
- Getting functional with closures and functions
- Introducing protocols and scratching the surface of extending protocols
- Concluding with other useful language constructs, such as type aliases, tuples, and generics
Let's start with a quick refresher on classes and structures. Both of them help to encapsulate functionality by defining methods and properties. While they share the same semantics, they differ in many ways. In this section, we'll quickly refresh you on the differences between classes and structs, and we will show you a simple refactoring from classes to structs.
Let's start with an example of a simple class that represents a Point
in an x
, y
coordinate system (Cartesian). Consider the following:
class Point { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.y = y } }
Now, let's define a simple translate
function that will mutate the x
and y
properties of the point
objects by adding dx
and dy
to x
and y
, respectively:
func translate(point : Point, dx : Double, dy : Double) { point.x += dx point.y += dy }
Now, we can create a point instance with, for example, an initial value of 0.0
, and translate it to the position 1.0
:
let point = Point(x: 0.0, y: 0.0) translate(point: point, dx: 1.0, dy: 1.0) point.x == 1.0 point.y == 1.0
Because classes follow reference semantics, only a reference to the point object is passed to the translate function; x
and y
are defined as var
, and all of this code is valid.
Now, let's try to port our Point
class into a struct
. Consider the following:
struct Point { var x: Double var y: Double }
We have defined a simple struct
; as you should notice, there's no need to add a constructor, as the compiler will synthesize it for us:
let point = Point(x: 0.0, y: 0.0) translate(point: point, dx: 1.0, dy: 1.0)
If we keep the original implementation, our program won't compile. Point
is a value type now, and it's forbidden to mutate a value inside of a function! We need to add the inout
keyword to indicate that this function will mutate the contents of the value that is passed. When the function returns, the value will be assigned back to the original variable.
With those changes complete, we also need to change our call to indicate that our point
variable can be modified by our translate
function with the &
(ampersand) character. We also need to mark our point as var
; otherwise, the inout
function cannot modify its contents:
func translate(point: inout Point, dx : Double, dy : Double) { point.x += dx point.y += dy } var point = Point(x: 0.0, y: 0.0) translate(&point, dx: 1.0, dy: 1.0) point.x == 1.0 // true point.y == 1.0 // true
We've successfully ported this function, but we can do better.
With structs, you will often see that this pattern is cumbersome. We may want the translate
function to return a mutated copy of the value we passed in, as follows:
func translate(point: Point, dx : Double, dy : Double) -> Point { var point = point translate(point: &point, dx : dx, dy : dy) return point }
We'll be able to use the previously defined function with the following code:
let point = Point(x: 0.0, y: 0.0) let translatedPoint = translate(point, dx: 1.0, dy: 1.0) point.x == 0.0 point.y == 0.0 translatedPoint.x == 1.0 translatedPoint.y == 1.0
With this new implementation, we're not mutating the value anymore, but the translate
function is always returning a new Point
value. This has many benefits, including the ability to chain such calls together. Let's add a method to our Point
struct:
extension Point { func translating(dx: Double, dy: Double) -> Point { return translate(point: self, dx: dx, dy: dy) } }
Note
You don't need to declare this new method in your struct
, but you can declare it anywhere in your program.
Using our newly crafted extension, we can easily create new Point
values and translate them:
let point = Point(x: 0.0, y: 0.0) .translating(dx : 5.0, dy : 2.0) .translating(dx : 2.0, dy : 3.0) point.x == 7.0 point.y == 5.0
Enums are one of the basic constructs that the Swift language offers. At the same level as classes, structs, and functions, they are used to represent values that can only have a finite amount of states.
Take the Optional
enum, for example; it is represented by an enum perfectly. It represents a value that can have two, and only two, states, represented by the two members of the Optional
enum. It can either be initialized to .none
or filled with a value, .wrapped(value)
.
Enums are incredibly powerful in Swift. From very simple cases to generics, they are among the most powerful tools that we have for writing our programs.
Let's say you're building a smart light remote control; you can easily represent the state of this light with the following enum
:
enum State { case on case off } let anOnLight = State.on
This is a very simple example, and we could have used a Boolean value, but with the enum
, we set ourselves up for expansion.
Now, we may want to add a method to this State
enumeration. After all, it's very common to just toggle the switch on and off without thinking:
extension State { mutating func toggle() { self = self == .off ? .on : .off } } var state: State = .on state.toggle() state == .off // true
As in the previous section, we can just extend the State
enum to add the toggle
functionality. Enums follow value semantics; therefore, we have to mark the toggle
method as mutating.
Enums can also contain associated values. In our scenario, we can leverage this to represent a dimmer. A dimmer changes the intensity of the light, so we can represent it with a third member-the dimmed member:
enum State: Equatable { case on case off case dimmed(value: Double) }
You may have noticed that we needed to add the Equatable
conformance. This is required, as otherwise, the compiler can't synthesize equality anymore without our hint. This implementation works, but we lack a few things. First, not all Double
values are valid; we'd probably like to keep these in a reasonable span (between 0
and 1
, for example). But perhaps not all of our lights support such values between 0
and 1
. Others may want to support between 0
and a 100
or integers between 0
and 255
.
In the following example, we will build a fully generic light:
enum State<T>: Equatable where T: Equatable { case on case off case dimmed(T) } struct Bits8Dimming: Equatable { let value: Int init(_ value: Int) { assert(value > 0 && value < 256) self.value = value } } struct ZeroOneDimming: Equatable { let value: Double init(_ value: Double) { assert(value > 0 && value < 1) self.value = value } } let nostalgiaState: State<Bits8Dimming> = .dimmed(.init(10)) let otherState: State<ZeroOneDimming> = .dimmed(.init(0.4))
The dim type is now specified as a part of the State
type. This gives us a lot of flexibility, as well as validation. Wrapping the value into a small struct
adds very little overhead in terms of performance, and allows us to ensure that the values are sane before being set into our enum
.
A raw type is a base type for all enumeration members; in our example, we can hardcode presets for our dimming, as follows:
enum LightLevel: String { case quarter case half case threequarters } let state: State<LightLevel> = .dimmed(.half)
Thanks to the generic implementation and the fact that String
is equatable, we can use this raw value in our dimmed
state.
With the LightLevel
enum, which has a raw type of String
, the compiler will use the member name as the underlying raw value:
LightLevel.half.rawValue == “half” // == true
You can override these by specifying them, as follows:
enum LightLevel: String { case quarter = “1/4” case half = “1/2” case threequarters = “3/4” }
When using Int
as a raw type, the underlying raw values will follow the order of the cases:
enum Level: Int { case base // == 0 case more // == 1 case high = 100 case higher // == 101 }
With our final case, let's look at how to interpret the current state of the light:
switch state { case .on: doSomething() case .off: doSomething() case .dimmed(let value): switch value { case .quarter: doSomething() case .half: doSomething() case .threeQuarters: doSomething() } }
The switch
statement in Swift is very different from the one in Objective-C. First, the cases do not fall through each other, so there's no need to add the break
statement after each case.
If you want multiple cases to be handled with the same code, you can use the following strategy:
switch state { case .on, .off: doSomething() default: break }
Falling through is somehow not encouraged in Swift, so always try to adapt your code in order not to leverage this. If you can't avoid it, the following code shows how it should be implemented:
switch state { case .off: doSomethingOff() fallthrough case .on: doSomething() default: break }
If state
is off
, both doSomethingOff
and doSomething
will be called. If state
is on
, only doSomething
will be called.
Closures are blocks of code that can be executed later, and functions are a special case of closures. Functions and closures can be passed around in your code, returned by other functions or closures. You can store a closure or a function in a variable, and execute them later:
let runMe = { () -> Int in print(“run”) return 0 } runMe()
The preceding code is equivalent to the following:
func runMe() -> Int { print(“run”) return 0 } runMe()
Closures and functions are almost always interchangeable, except when it comes to class
or struct
members:
class MyClass{ var running = false lazyvar runWithClosure: () -> Void = { self.running = true } func runWithFunction() { self.running = true } }
While both implementations are somewhat equivalent, we rarely want this function to be overridable at runtime. The closure can't reference self
inside of it, unless marked lazy
. Marking it lazy
forces the implementation to be var
, which, in turn, doesn't reflect what we want to express. In practice, we never declare instance methods as closures.
Functions and closures don't have to be defined at the top level. This can be unintuitive, when coming from languages such as Objective-C and Java. Swift, like JavaScript, lets you define functions and closures anywhere in your code. Functions can also return functions. This mechanism is known as currying.
Imagine that you want to create a logger
method that will print a single argument, but it will always pretend to be a string to find it easily in your logs.
Let's start with the following basic implementation:
private let PREFIX = ‘MyPrefix' private func log(_ value: String) { print(PREFIX + “ “ + value) } class MyClass { func doSomething() { log(“before”) /* complex code */ log(“after”) } }
While this works properly in the scope of a simple class, if you need to reuse the log
method or change the internal implementation, this will lead to a lot of duplication.
You can use currying to overcome that issue, as follows:
func logger(prefix: String) -> (String) -> Void { func log(value: String) { print(prefix + “ “ + value) } return log } let log = logger(prefix: “MyClass”) log(“before”) // do something log(“after”) // console: MyClass before MyClass after
Functions and closures can capture the current scope, which means all of the declared variables outside of the function or closure definition, such as local variables or self
. In the case of self
, you can inadvertently extended the lifetime of your objects and leak memory:
class MyClass { var running = false func run() { running = true DispatchQueue.main.asyncAfter(deadline: .now() + 10) { self.running = false } } } var instance: MyClass? = MyClass() instance?.run() instance = nil
Can you spot the potential issue in this code?
Depending on the use case, you may want instance
to be destroyed when it is not referenced by any owner. In our case, we'll probably cause a memory leak, as the dispatch block is referencing self
without any memory management qualifier.
Swift provides us with two keywords that indicate how we want to extend the lifetime of an object in a closure. While both prevent creating retain cycles, they are fundamentally different.
Using weak
will wrap the captured value inside of an optional, indicating that the instance may have been deallocated before the closure was executed:
class MyClass { var running = false func run() { running = true DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in self?.running = false } } } var instance: MyClass? = MyClass() instance?.run() instance = nil
In this execution, instance
will immediately be deallocated when set to nil
.
Using unowned
indicates that the variable won't be owned by the block. Another mechanism should be responsible for ensuring that the lifetime of the captured object is properly extended until the block is executed:
class MyClass { var running = false func run() { running = true DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [unowned self] in self.running = false } } } var instance: MyClass? = MyClass() instance?.run() instance = nil
In this case, your program will crash when the block is executing, because the self
variable will be deallocated upon the execution of the block:
Fatal error: Attempted to read an unowned reference but object 0x7f80bc75a4e0 was already deallocated
The following is from Apple's Swift Programming Language book:
"A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol."
– Apple Inc., The Swift Programming Language (Swift 3.0.1), iBooks
Protocol-oriented programming is a vast topic that also deserves coverage. It is the subject of many discussions, and I won't dive into it in depth. However, let's go over the basic concepts, as they will be useful for understanding some concepts that will be explained later in this book.
You declare protocols using the protocol
keyword, as follows:
protocol Toggling { mutating func toggle() }
Now that this protocol has been declared, any time that you declare a type conforming to Toggling
, you'll be required to implement a mutating toggle()
function.
You can use protocols in your type declarations, method declarations, or variable declarations. While it is technically possible to use protocols as interfaces for your objects or structs, that is usually not how they are used in Swift. Often, you will find yourself conforming to protocols when declaring your custom types or later in your code base, part of extending your existing type to bring additional functionality to it.
We have just declared this new toggling protocol. If we go back to the previous section about enums, you may remember that the State
enum had a toggle()
method. We can now declare that our enum
, State
, conforms to Toggling
. As mentioned previously, we have many ways to declare our conformance.
The first method to declare a conformance is to do it at the top level, when you declare your custom type. You'll notice that the raw representation comes first, then the protocol conformance:
enum State: Int, Toggling { case off = 0 case on mutating func toggle() { self = self == .on ? .off : .on } } var state: State = .on state.toggle() assert(state == .off)
The second way to declare a conformance is to add the conformance to an extension. The main benefit is that you can add functionalities, in the form of extensions, to existing types. The other main benefit of declaring a conformance inside of an extension is that you can scope this conformance to a particular file or module, with the private
modifier.
For example, suppose that we want to add the toggle
method to the Bool
type, but only for the current file or your framework. You may not want it to leak outside, as the implementation may conflict with another one:
internal extensionBool: Toggling { mutatingfunc toggle() { self = !self } } var isReady = false isReady.toggle() assert(isReady)
With protocol extensions, it is possible to provide an implementation for the required methods, without letting the conforming types provide that implementation.
We have updated the Toggling
protocol with an additional required member: isActive
. With the protocol extension, we can declare a default implementation for our types, Bool
and State
. We can also provide a default implementation for any other type that would choose to conform to the Toggling
protocol:
protocol Toggling { mutating func toggle() var isActive: Bool { get } } extensionTogglingwhereSelf == Bool { var isActive: Bool { returnself } } extensionTogglingwhereSelf == State { var isActive: Bool { returnself == .on } }
It is possible to provide default implementations for protocols through extensions. Previously, we provided a partial default implementation for the Toggling
protocol on a well-known type. But any other type, that would conform toToggling
needs to provide an implementation on isActive
. Using another example, let's look at how we can leverage default implementations in protocol extensions without requiring additional conformance work.
Let's work with a simple protocol, Adder
, for the sake of the example:
protocol Adder { func add(value: Int) -> Int func remove(value: Int) -> Int }
The Adder
protocol declares two methods: add
and remove
. And, if we remember our math classes well, we can very well declare remove
as a function of add
. Removing is just adding a negative value. Protocol extension allows us to do just that:
extensionAdder { func remove(value: Int) -> Int { returnadd(value: -value) } }
This may look a bit silly, but in reality, this pattern is really powerful. Remember, we were able to implement remove
because we were able to express it as a function of another provided method. Often, in our code, we can implement a method as a function of another. Protocols give us a contract that is fulfilled by either the concrete type or the extension, and we can effectively and expressively compose our programs around those capabilities.
This chapter would not be complete if we didn't address some very useful features from Swift. Tuples are very useful types that let you return multiple objects as one, without a strongly typed wrapper. Aliases let you quickly define simple type shortcuts. Finally, we'll cover the basics of generics. While generics could be covered in a whole book, we'll just scratch the surface of their syntax, features, and limits, as we'll make use of them extensively throughout this book.
Tuples are used to represent a group of values as a single value. Tuples cannot conform to protocols, nor can they inherit. They cannot declare functions in the same way that we can declare a function on a struct
or a class
. They may look limited, but they have their place as first-class types in the language.
Tuples can hold any number of values, from any number of types. You can declare a tuple with the same types—let's say a 2D point in Double
:
let origin = (0.0, 0.0)
You can also name the parameters, as follows:
let point = (x: 10.0, y: 10.0)
The two forms are equivalent, but you may want to use the named version, for readability reasons. If you're referencing a size, for example, the tuple would more accordingly be named (width: Double, height: Double)
. For obvious reasons, this helps to provide a better understanding of your code.
There is a simple method to access tuple values. Take, for example, the size
pair, as follows:
let size = (width: 200, height: 400) let (w, h) = size let (width, _) = size
In the preceding example, we initialize a tuple on the first line. On the second line, we destructure both parameters as w
and h
. On the last line is what we call a partial destructuring: when you're only interested in one part of the tuple, you can extract only a part of it. This is useful when dealing with large tuples.
Tuples are first-class citizens in Swift; you can use them, like any other type, as function parameters. The following code demonstrates how to declare a simple function that computes to the Euclidean distance between two points, a
and b
, represented by tuples:
func distance(_ a: (Double, Double), _ b: (Double, Double)) -> Double { returnsqrt(pow(b.0 - a.0, 2) + pow(b.1 - a.1, 2)) } distance(point, origin) == 5.0
You may have noticed that the named parameters of the point
tuple are ignored in this case; any pair of Double
will be accepted in the method, no matter what they are named.
The opposite is true, as well:
func slope(_ a: (x: Double, y: Double),_ b: (x: Double, y: Double)) -> Double { return (b.y - a.y) / (b.x - a.x) } slope((10, 10), (x: 1, y: 1)) == 1
We've seen examples of using tuples with the same types, but remember that, tuples can contain any type, and as many values as you wish.
Type aliases are a simple addition to the language; they let you reference simple or complex types by an alias. They support all declarations that you can imagine, from the simplest to the most complex.
The following block contains declarations for aliasing the following:
- A string class into a
MyString
- A function declaration into a
Block
- A block that takes any argument and returns any value
- A block that takes no argument and returns any value
Let's see the code block; they let you:
typealias MyString = String typealias Block = () -> Void typealias TypedBlock<T, U> = (T) -> U typealias ReturningBlock<U> = () -> U
We could have also defined Block
in the function of ReturningBlock
:
typealias Block = ReturningBlock<()>
You can also use type aliases for protocol compositions and complex types, as follows:
- You can declare a type that conforms to a protocol and is of a particular class
- You can delete a type that conforms to multiple protocols
Let's see an example, as follows:
protocol SomeProtocol {} protocol OtherProtocol {} typealias ViewControllerProtocol = NSViewController & SomeProtocol typealias BothProtocols = SomeProtocol & OtherProtocol
You will often find yourself using type aliases, in order to make your code more readable and more expressive. They are a powerful tool for hiding away some of the implementation complexity or verbosity when declaring long conformances. With type aliases, you can be encouraged to craft many protocols, each with a very small requirement list; then, you can compose all of those protocols when you need them, expressed as those types.
Generics is a complex subject, and would likely require a full book of its own, for extensive coverage extensively. For the purpose of this book, we'll provide a quick refresher on generics, covering the basics that are required to understand the constructions that we'll use in the different design patterns presented in the next chapters.
In Swift, the simplest form of generics would be the generics in functions. You can use generics very simply, with angled brackets, as follows:
func concat<T>(a: T, b: T) -> [T] { return [a,b] }
The concat
method knows nothing about the types that you are passing in, but generics gives us many guarantees over using Any
:
a
andb
should be of the same type- The
return
type is an array of elements that have the same type asa
andb
- The type is inferred from the context so you don't have to type it in when you code
You can also leverage protocol conformance in your generic functions, as follows:
protocol Runnable { func run() } func run<T>(runnable: T) where T: Runnable { runnable.run() }
In this case, the method that is run can only be called with an object that is Runnable
.
You can also make complex types generic. In our example, we created this wrapper around a list of Runnable
, called ManyRunner
. The job of a many runner is to run all of the runnables. The ManyRunner
is itself Runnable
, so we have created a kind of type recursion, as follows:
struct ManyRunner<T>: Runnable where T: Runnable { let runnables: [T] func run() { runnables.forEach { $0.run() } } }
Let's also provide a base object that runs a simple Incrementer
. Each time the Incrementer
is run, the static count will increment, to keep track of the number of invocations:
struct Incrementer: Runnable { private(set) static var count = 0 func run() { Incrementer.count += 1 } }
When using generics on types, remember that the types have to be the same:
// This works let runner = ManyRunner(runnables: [Incrementer(),Incrementer()]) runner.run() assert(Incrementer.count == 2) // runner is of type ManyRunner<Incrementer> ManyRunner(runnables: [Incrementer(), Runners(runnables: [Incrementer()])] as [Runnable]).run() // This produces the following compile error // In argument type '[Runnable]', 'Runnable' does not conform to expected type 'Runnable'
We'll look at how to overcome these limitations in Chapter 8, Swift-Oriented Patterns.
You can also use associated types in your protocols. These associated types let you define protocols that are generics, like this: RunnableWithResult
. We can implement a bunch of logic and code around the run()
method, without actually knowing anything about the return types. We'll encounter this construction many times in this book, so it's important that you're comfortable with associate types:
protocol RunnableWithResult { associatedtype ResultType func run() -> ResultType } struct RunnersWithResult<T>: RunnableWithResult where T: RunnableWithResult { let runnables: [T] func run() -> [T.ResultType] { return runnables.map { $0.run() } } }
Like with generic types, you can't mix and match heterogeneous types. The following example will not compile; later in this book, you'll see strategies for overcoming this common problem when dealing with generics:
struct IntRunnable { func run() -> Int { return 0 } } struct StringRunnable { func run() -> String { return "OK" } } let runnables: [RunnableWithResult] = [StringRunnable(), IntRunnable()]
This will yield the following dreaded error:
Protocol 'RunnableWithResult' can only be used as a generic constraint because it has Self or associated type requirements
In this chapter, we covered everything that I consider a prerequisite for the rest of this book. We started with classes, the basic building blocks of OOP. You should now be really familiar with them. Structs are unusual constructions for someone coming from OOP, but they are very useful in Swift, as they behave as values, can be immutable, and have other nice properties. With enums, you'll be able to write even more expressive code.
Functions and closures are first-class citizens in Swift, and should be treated as such. Currying is a powerful pattern that lets you reuse functions; in later chapters, you'll see how to use it to write clean code.
The concept of protocols opens the world of protocol extensions and protocol-oriented programming, which is a complex subject. In the following chapters, we'll look at various use cases for implementing particular patterns through protocol extensions.
In the next chapter, we'll focus on memory management and ARC. While value types are not subject to reference counting, classes, functions, and closures interact with each other, and can lead to memory-related crashes and other issues.