Welcome to the “Mastering Swift” tutorial series! This tutorial is a bit different from the ones usually featured on AppCoda, because instead of teaching you about the iOS APIs or a specific iOS topic, this tutorial will teach you about Swift, Apple’s new programming language for developing apps. We will be exploring some tips, tricks, and techniques that you can follow to make your Swift code even Swiftier. Swift was designed with safety, clarity, and stability in mind, and we will use several of Swift’s key features to achieve these goals.
To get started, fire up a new Xcode and create a Playground file. You won’t need a starter project to follow this tutorial. I will just walk you through the code and test it using Playgrounds.
Enumerations
If you don’t know about them already, enumerations, or enums, are a special type of value in Swift that allow you to represent multiple cases, or possibilities. Enums are similar to Bool
in that enum values have to be one of multiple cases. Bool
can only be true
or false
, but enums can be any of the cases you define. Let’s take a look.
Assuming you have open Xcode’s Playgrounds, we’ll start by declaring an enum:
enum DownloadStatus {
case downloading
case finished
case failed
case cancelled
}
As you can see, declaring an enum is easy. In our example above, we have declared a new enum called DownloadStatus
with 4 cases, downloading
, finished
, failed
, and cancelled
. We can use our enum just like we use any other type such as String
or Int
:
var currentStatus = DownloadStatus.downloading
Editor’s note
At this point, you may wonder why you need to use enum to define the direction. Probably you’re thinking why you can’t declare direction as an array with four items like this:
let downloadStatus = [“downloading”, “finished”, “failed”, “cancelled”]
let currentStatus = downloadStatus[0]
Yes, you can do that. But there are two disadvantages here. First, you have no idea about what downloadStatus[0] is, unless you refer to the downloadStatus array. If you compare downloadStatus[0] to DownloadStatus.downloading, it is very obvious that the latter is more readable.
Secondly, as currentStatus is of type String, you can actually assign whatever string value to the variable. You can’t limit the value to “downloading”, “finished”, “failed” and “cancelled”, unless you perform some additional validations. On the other hand, if enum is used, we can only set myDirection to .downloading, .finished, .failed or .cancelled, but nothing else.
When declaring enums, the actual name of the enum (DownloadStatus
in this case) should begin with a capital letter. Additionally, it should be singular. This means that naming our enum downloadStatus
or DownloadStatuses
would be grammatically incorrect. It’s important to follow consistencies and conventions when programming, and this is one that should be universally followed.
Additionally, our enum cases should begin with lowercase letters, as of Swift 3. Prior to Swift 3, the convention was to capitalize enum cases, but that has changed. If you’re developing with Swift 3 or higher, be sure that your enum cases begin with lowercase letters.
Now that you know what enums are, let’s explore how to use them. Swift allows us to switch
over enums. Take a look:
let currentStatus = DownloadStatus.downloading
switch currentStatus {
case .downloading:
print("Downloading...")
case .finished:
print("Just finished the download...")
case .failed:
print("Failed to download the file...")
case .cancelled:
print("The download is cancelled...")
}
This allows us to write a conditional statement that’s much more powerful than a simple if
statement. The compiler will enforce that we handle each of the cases in our enum in our switch
statement, making sure that we don’t leave out any possible cases. This contributes to the overall safety of our code, making sure we don’t miss anything.
Up until this point, you may be thinking that enums don’t really add new functionality to Swift. Sure, they can make our code safer, but you could always use a String
or a few Bool
s to represent data stored by an enum. Now, let’s take a look at one of the most powerful features of Swift enums: associated values. Associated values allow us to store additional data within an enum. Let’s declare a new enum, called WeatherCondition
, that allows us to specify a weather condition, along with some extra information:
enum Cloud {
case cirrus
case cumulus
case altocumulus
case stratus
case cumulonimbus
}
enum WeatherCondition {
case sunny(temperature: Float)
case rainy(inchesPerHour: Float)
case cloudy(cloudType: Cloud, windSpeed: Float)
}
In this example, we actually declared 2 enums: Cloud
and WeatherCondition
. Don’t pay much attention to the Cloud
enum. Instead, look at the WeatherCondition
declaration. We declared 3 cases, and each of them stores some kind of additional information aside from the case of the enum itself. In our sunny
case, we store an Int
, called temperature
. In our rainy
case, we store a Float
, called inchesPerHour
, in our cloudy
case, we store 2 values: cloudType
, a Cloud
, and windSpeed
, a Float
.
As you may have observed, associated values let us store additional information in our enums very easily. Let’s see how to use them:
let currentWeather = WeatherCondition.cloudy(cloudType: .cirrus, windSpeed: 4.2)
We can switch
on associated value enum cases, just like we can on normal ones:
switch currentWeather {
case .sunny(let temperature):
print("It is sunny and the temperature is \(temperature).")
case .rainy(let inchesPerHour):
print("It is raining at a rate of \(inchesPerHour) inches per hour.")
case .cloudy(let cloudType, let windSpeed):
print("It is cloudy; there are \(cloudType) clouds in the sky, and the wind speed is \(windSpeed).")
}
Hopefully, you can see the value that enums add to your app. They can make your code safer, clearer, and more concise. Maybe you already knew about enums, but were unaware of the assocated values they offer, or how to switch
over them. Regardless, try to incorporate them into your apps, as they’re incredibly useful.
Closures and Higher Order Functions
One of Swift’s key features is the closure. In Objective C, closures are known as blocks. The concept is similar, but they’re much more powerful in Swift. Simply put, a closure is a function without a name. Swift closures are first class types, meaning that they can be assigned to variables just like any other type. Closures can also be passed into functions.
The type of a closure is expressed as (parameters) -> returnType
. For example, a closure that takes a String
and a Float
as parameters and returns Void
has the following type: (String, Float) -> Void
. You can write functions that accept closures as parameters:
func myFunction(_ stringParameter: String, closureParameter: (String) -> Void) {
closureParameter(stringParameter)
}
The function above takes a string and a closure, and then it calls the closure, providing the string parameter to the closure. Here’s an example of using it:
myFunction("Hello, world!", closureParameter: {(string) in
print(string) //prints "Hello, world!"
})
In this case, we provide the closure to the function just like we provide a normal parameter. Then, within the function, we call the closure, just like we would call any other function declared with func
. This example illustrates that closures really are just unnamed functions. As a matter of fact, closures in other languages are sometimes referred to as anonymous functions, because that’s really what they are.
Swift also has something called trailing closure syntax, which is syntax sugar for closures. This allows for cleaner and more elegant closure expressions when a closure is the last parameter of a function. Here’s an example, using the same function:
myFunction("Hello, world!") {(string) in
print(string) //prints "Hello, world!"
}
Trailing closure syntax lets us eliminate the parentheses around the closure parameter in a function, but only if it’s the last parameter. When writing functions, make sure you put the closure parameter last (if you have one) so that people who call your code can do so in a clean and concise manner.
Because closures in Swift are so powerful, the Standard Library provides higher order functions. Put simply, a higher order function is a function that takes another function as a parameter. HOFs are present on collection types in Swift, offering functions such as map
, filter
, forEach
, reduce
, and flatMap
. We will be walking through each one now.
Map
Let’s start with map
. Type the following code in Playgrounds and see what you get.
let mapNumbers = [1, 2, 3, 4, 5]
let doubledMapNumbers = mapNumbers.map { $0 * 2 }
print(doubledMapNumbers) //prints [2, 4, 6, 8, 10]
In the code above, it is an example of the map
function. As you can see, it is so simple to multiple each of the item in the mapNumbers
array by 2 via the map
function. You can do that using for
loop. The map
function simply saves you a lot of code.
The map
function takes an input closure, which has one parameter of the same type as the Element
of the collection being mapped. This closure should also return a value of the same type. In our case, the parameter is unnamed, meaning we do not explicitly provide a name for it. For this reason, we can refer to it as $0
. Subsequent unnamed parameters can be referred to as $1
, $2
, $3
, and so on.
On top of that, you may notice that there is no return value of the closure. For one line closures, we don’t need to use the return
keyword, as it’s implied. Finally, we use trailing closure syntax, which allows us to decrease the length of our code further. All of these syntactical shortcuts allow us to write really short, concise closure expressions. Here’s what the same expression let doubledMapNumbers = mapNumbers.map { $0 * 2 }
would look like without these syntax improvements:
let doubledMapNumbers = mapNumbers.map( {(number) in
return number * 2
})
As you can see, Swift provides us with a lot of ways to shorten our closure expressions, allowing for clean code that’s easy to read.
Filter
Let’s explore more higher order functions. Declare another array, filterNumbers
.
let filterNumbers = [1, 2, 3, 4, 5]
let filteredNumbers = filterNumbers.filter { $0 > 3 }
print(filteredNumbers) //prints [4, 5]
In this example, we filter filteredNumbers
, keeping only the items that are greater than 3. This example also makes use of Swift’s various syntax improvements to make sure that our code is concise.
forEach
Let’s try forEach
, with an array called forEachNumbers
:
let forEachNumbers = [1, 2, 3, 4, 5]
forEachNumbers.forEach { print($0) } //prints one item of the array on each line
Using forEach
is very similar to using a for
loop. Again, we used many syntax improvements to make sure we have clean code.
Reduce
Now, let’s give reduce
a try, with reduceNumbers
:
let reduceNumbers = [1, 2, 3, 4 ,5]
let reducedNumber = reduceNumbers.reduce(0) { $0 + $1 }
print(reducedNumber) //prints 15
reduce
is used to reduce a collection down into a single value. In our case, we add all of the numbers together in reduceNumbers
into one value, and store it in reducedNumber
.
The parameter (i.e. 0
) we provide for the reduce
function is an initial value. reduce
starts with our initial value and performs the specified operation for each item in the collection.
0 + 1 + 2 + 3 + 4 + 5 is 15, which is why print(reducedNumbers) gives you 15.
flatMap
One more to go! Let’s use flatMap
now. Declare an array called flatMapNumbers
. This time, our starting array is a little different than the earlier example that it contains nil
values:
let flatMapNumbers = [1, nil, 2, nil, 3, nil, 4, nil, 5]
let flatMappedNumbers = flatMapNumbers.flatMap { $0 }
print(flatMappedNumbers) //prints [1, 2, 3, 4, 5]
flatMap
traverses through a collection, using the input closure to manipulate values. The resulting collection only contains non-nil values. You’ll notice that that the resulting array contains no nil
elements. flatMap
is particularly useful for removing optional values from a collection.
Chaining Them Together
Okay, I promised that flatMap
would be the last example, but I have one more to show you, and it’s actually a combination of several transforms. You can chain higher order functions to create powerful transformations that aren’t possible with one transform. Here’s an example:
let chainNumbers = [1, nil, 2, nil, 3, nil, 4, nil, 5]
let doubledNumbersOver8 = chainNumbers.flatMap { $0 }.filter { $0 > 3 }.map { $0 * 2 }
print(doubledNumbersOver8) //prints [8, 10]
Hopefully, these examples have illustrated the power of closures and higher order functions in Swift. You can use these techniques in your apps to clarify your code and make it easier to maintain. Good luck!
Generics
Generics are an interesting concept, and they allow you to reuse your code for multiple types. Let’s look at an example:
func swapInts(_ a: inout Int, _ b: inout Int) {
let temporaryB = b
b = a
a = temporaryB
}
Our swapInts
function takes 2 Int
s and swaps them. Here is a sample usage:
var num1 = 10
var num2 = 20
swapInts(&num1, &num2)
print(num1) // 20
print(num2) // 10
But what if we want to swap strings? Probably you will write another function like this:
func swapStrings(_ a: inout String, _ b: inout String) {
let temporaryB = b
b = a
a = temporaryB
}
As you can see, both functions are identical, except for the fact that they have different parameter types. Can you write a swap function that supports multiple types of parameters? This is where we can apply Generics to write more flexible code. We can turn the swap function into a more generic function like this:
func swapAnything(_ a: inout T, _ b: inout T) {
let temporaryB = b
b = a
a = temporaryB
}
Let me explain what we just wrote. We started by declaring what looks just like a normal Swift function, but, as you can see, we have the letter T in between angle brackets. What does this mean? Angle bracket’s are Swift’s syntax for generics. By putting the letter T in between angle brackets, we’re telling Swift that we are forming a generic type, called T
. We can now reference this type T
in our function. As you can see, we use it to denote the types of our parameters. The rest of the function is just basic declarations. We can now use our swapAnything
function to swap anything we want.
var string1 = "Happy" var string2 = "New Year" swapAnything(&string1, &string2) print(string1) // New Year print(string2) // Happy var bool1 = false var bool2 = true swapAnything(&bool1, &bool2) print(bool1) // true print(bool2) // false
Let’s look at a more sophisticated example of using generics. But first, let’s look back. For those of you who are familiar with Objective C, remember how items accessed from an NSArray
would be returned as id
? If we write this:
NSArray *myArray = @[1, 2, 3, 4, 5]; int myInt = (int)myArray[2];
We would always have to cast elements from the array into their actual types manually. This is unsafe. What if you don’t know what types of items are in the array? What type would you cast it to? What if you forget to cast it and you attempt to use the object anyway? Your app might crash because of an undefined selector or similar issue.
This is where Swift’s Generics come to rescue. Let’s take a look at the following example:
struct Stack {
private var storage = [Any]()
mutating func push(_ item: Any) {
storage.append(item)
}
mutating func pop() -> Any? {
return storage.removeLast()
}
}
var myStack = Stack()
myStack.push("foo")
myStack.push(5)
myStack.push(4.7)
let element = myStack.pop()
Here, we declare a stack, that allows you to push any items to it, and pop an item off.
There’s one issue here. The item stored in the stack, and the element
we pop, is of type Any
. How do we know what type the element
is? The right answer is…we don’t know. It could be a String, an Int, or a Float.
What if we have no idea about the items in the stack? What can we do? Every time when you need to use element
, you will probably need to discover its type like this:
switch element { case let number as Int: print(number) case let number as Double: print(number) case let text as String: print(text) default: print("Unknown type") }
It doesn’t seem like a good solution, and it is not a good practice to store items in an array of Any
type in Swift. A better way to do it is to limit the stack to a single type. In this case, we will have to create a stack for String, another for Int, another for String. And here, generics in Swift will allow you to create a generic stack:
struct GenericStack {
private var storage = [Element]()
mutating func push(_ item: Element) {
storage.append(item)
}
mutating func pop() -> Element? {
return storage.removeLast()
}
}
It isn’t much more complex than our original stack, but the stack is now type safe. Let’s see it in action:
var textStack = GenericStack()
textStack.push("foo")
textStack.push("bar")
textStack.push("baz")
let textElement = textStack.pop() // baz
var numStack = GenericStack()
numStack.push(10)
numStack.push(20)
numStack.push(30)
let numElement = numStack.pop() // 30
Now that we use generics, our pop
function returns a String
instead of Any
. This adds a lot of type safety to our code and it eliminates unnecessary casting.
Hopefully, you’ll find a way to use generics in our own apps. They’re extremely powerful and they can be used to solve a lot of problems elegantly. Give them a try!
Value vs Reference Types
If you’ve been working with Swift since the early days, you may have heard about the Standard Library’s extensive use of value types. If you’re wondering what they are, now is a good time to find out, as the language is maturing and it doesn’t look like value types are going away anytime soon. To understand value types, you need to understand reference types. Let’s take a look at an example for Objective C:
NSString *myString = @"Hello, world!";
NSString *myOtherString = myString;
myString = @"Guess what? We have a problem.";
NSLog(myOtherString); //prints "Guess what? We have a problem."
Uh oh! What happened? This is a prime example of the danger of reference types. You see, when we set myOtherString
to myString
, the compiler didn’t copy the value of myString
and provide one copy to myOtherString
. Instead, the compiler told myOtherString
to reference myString
. When myString
changed, so did myOtherString
. This type of referencing can cause a lot of bugs if developers do not use it carefully, so the designers of the Swift language chose to use value types instead. Let’s look at the same example in Swift:
var myString = "Hello, world!"
let myOtherString = myString
myString = "No problems here."
print(myOtherString) //prints "Hello, world!"
Great! No more bugs about weird reference semantics. This makes most code easier to write and maintain for developers. In Swift, struct
s are value types, and class
es are reference types. This knowledge isn’t immediately useful in most day to day development work, but it’s helpful to understand. It can also help you make the choice when deciding between a class and a struct.
Protocols
Swift has another interesting feature known as protocol. Protocols are a new way of viewing class hierarchy and inheritance. In a conventional object oriented programming environment, you define classes, which describe objects and the functionality they provide, along with the properties they have. Then, you subclass your classes, inheriting all of their functionality and properties. Your subclasses can then provide additional functionality and properties, or override parts of their superclasses. These subclasses can then be subclassed further. Let’s look at an example of a typical class hierarchy:
class Animal {
func makeSound() {
print("Implement me!")
}
func move() {
print("Implement me!")
}
}
class Dog: Animal {
override func makeSound() {
print("Woof.")
}
override func move() {
print("walk around like a dog")
}
func bite() {
print("bite")
}
}
class Cat: Animal {
override func makeSound() {
print("Meow.")
}
override func move() {
print("walk around like a cat")
}
func scratch() {
print("scratch")
}
}
This example illustrates a hierarchy in which we define a base class, Animal
, and then subclass it. The Dog
subclass overrides the default functionality of the Animal
class, and also adds a bite()
method, which allows dogs to bite things. The Cat
subclass also overrides the base class, but it adds a scratch()
method, allowing cats to scratch people.
Do you see any issues with this example? It may not seem like there is anything immediately wrong with this structure, but in reality, there are a lot of things that could be improved. What if someone else subclasses Animal
but forgets to override makeSound()
and / or move()
? In this case, it will just print “Implement me!”
class Tiger: Animal { func eat() { print("Eat like a tiger") } } let animal: Animal = Tiger() animal.makeSound() // Implement me! animal.move() // Implement me!
This is a simple example, but imagine what could happen if Animal
had dozens of functions and properties that needed to be overriden. What would happen?
In Objective C, these situations presented us with things called abstract base classes. Abstract base classes are like Animal
in the fact that they do not provide much functionality on their own. They provide a list of things that need to be overriden, without actually providing functionality on their own. A typical Objective C base class may throw an assertion or otherwise crash the program it’s in if it’s used directly. For this reason, abstract base classes are documented as abstract, and developers who use them are told to never interact with them directly, and instead interact with one of their subclasses.
In Swift, there is no abstract class. However, Swift (and Objective C) both provide a better way to approach situations such as this one: protocols. While protocols are present in both Swift and Objective C, Swift protocols are much more powerful. For the same problem, we can implement Animal
as a protocol like this:
protocol Animal {
func makeSound()
func move()
}
struct Dog: Animal {
func makeSound() {
print("Woof.")
}
func move() {
print("walk around like a dog")
}
func bite() {
print("bite")
}
}
struct Cat: Animal {
func makeSound() {
print("Meow.")
}
func move() {
print("walk around like a cat")
}
func scratch() {
print("scratch")
}
}
Now, what’s different? We still have a dog and cat which both provide the same functionality that their subclassed counterparts did. But, this time, these animals both adopt a common protocol. You see, a class defines what an object is, but a protocol defines what an object does. You can still use the protocol similar to what we did before:
let animal: Animal = Dog() animal.makeSound() animal.move()
Both Cat and Dog conform to the Animal
protocol. And, it is mandatory for them to implement both makeSound
and move
methods. When adopting a protocol, the compiler enforces that any class or struct should provide the implementation of the required method as specified in the protocol declaration. Therefore, it is impossible to have a struct or class like this:
The compiler will give you an error to make sure you provide the required implementation of the protocol.
We’ll discuss protocols further in an extension of this tutorial, showcasing more situations where protocols can be helpful. Just remember that neither subclassing nor protocols can solve every single programming problem. Sometimes, you need both. Don’t get too hung up on one or the other, as both are incredibly useful and powerful.
Wrapping up
I hope you learned a lot in this tutorial. We talked about a lot of important Swift topics that can enable us to do some really powerful things in our apps. Whether you learned about enums, closures, generics, reference / value types, protocols, or some combination of these items, I hope this tutorial taught you something that you can use every day. Feel free to comment below with any questions.