Book Image

Hands-On Design Patterns with Swift

By : Florent Vilmart, Giordano Scalzo, Sergio De Simone
Book Image

Hands-On Design Patterns with Swift

By: Florent Vilmart, Giordano Scalzo, Sergio De Simone

Overview of this book

Swift keeps gaining traction not only amongst Apple developers but also as a server-side language. This book demonstrates how to apply design patterns and best practices in real-life situations, whether that's for new or already existing projects. You’ll begin with a quick refresher on Swift, the compiler, the standard library, and the foundation, followed by the Cocoa design patterns – the ones at the core of many cocoa libraries – to follow up with the creational, structural, and behavioral patterns as defined by the GoF. You'll get acquainted with application architecture, as well as the most popular architectural design patterns, such as MVC and MVVM, and learn to use them in the context of Swift. In addition, you’ll walk through dependency injection and functional reactive programming. Special emphasis will be given to techniques to handle concurrency, including callbacks, futures and promises, and reactive programming. These techniques will help you adopt a test-driven approach to your workflow in order to use Swift Package Manager and integrate the framework into the original code base, along with Unit and UI testing. By the end of the book, you'll be able to build applications that are scalable, faster, and easier to maintain.
Table of Contents (22 chapters)
Title Page
Copyright and Credits
About Packt
Contributors
Preface
Index

Tuples, type aliases, and generics


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

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.

Declaring tuples

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.

Destructuring tuples

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.

Using tuples in functions

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

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

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.

Generic functions

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 and b should be of the same type
  • The return type is an array of elements that have the same type as a and b
  • 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.

Generic types

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 8Swift-Oriented Patterns.

Generics, protocols, and associated types

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