21.
Swift Review
Written by Eli Ganim
You have made great progress! You’ve learned the basics of Swift programming and created two applications from scratch, one of them in SwiftUI and the other in UIKit. You are on the threshold of creating your next app.
A good building needs a good foundation. And in order to strengthen the foundations of your Swift knowledge, you first need some additional theory. There is still a lot more to learn about Swift and object-oriented programming!
In the previous chapters, you saw a fair bit of the Swift programming language already, but not quite everything. Previously, it was good enough if you could more-or-less follow along, but now is the time to fill in the gaps in the theory. So, here’s a little refresher on what you’ve learned so far.
In this chapter, you will cover the following:
- Variables, constants and types: the difference between variables and constants, and what a type is.
- Methods and functions: what are methods and functions — are they the same thing?
- Making decisions: an explanation of the various programming constructs that can be used in the decision making process for your programs.
- Loops: how do you loop through a list of items?
- Objects: all you ever wanted to know about Objects — what they are, their component parts, how to use them, and how not to abuse them.
- Protocols: the nitty, gritty details about protocols.
Variables, constants and types
A variable is a temporary container for a specific type of value:
var count: Int
var shouldRemind: Bool
var text: String
var list: [ChecklistItem]
The data type, or just type, of a variable determines what kind of values it can contain. Some variables hold simple values such as Int or Bool, others hold more complex objects such as String or Array.
The basic types you’ve used so far are: Int for whole numbers, Float for numbers with decimals (also known as floating-point numbers), and Bool for boolean values (true or false).
There are a few other fundamental types as well:
-
Double. Similar to aFloatbut with more precision. You will useDoubles later on for storing latitude and longitude data. -
Character. Holds a single character. AStringis a collection ofCharacters. -
UInt. A variation onIntthat you may encounter occasionally. The U stands for unsigned, meaning the data type can hold positive values only. It’s called unsigned because it cannot have a negative sign (-) in front of the number.UIntcan store numbers between 0 and 18 quintillion, but no negative numbers. -
Int8,UInt8,Int16,UInt16,Int32,UInt32,Int64,UInt64. These are all variations onInt. The difference is in how many bytes they have available to store their values. The more bytes, the bigger the values they can store. In practice, you almost always useInt, which uses 8 bytes for storage on a 64-bit platform (a fact that you may immediately forget) and can fit positive and negative numbers up to about 19 digits. Those are big numbers! -
CGFloat. This isn’t really a Swift type but a type defined by the iOS SDK. It’s a decimal point number likeFloatandDouble. For historical reasons, this is used throughout UIKit for floating-point values. (The “CG” prefix stands for the Core Graphics framework.)
Swift is very strict about types, more so than many other languages. If the type of a variable is Int, you cannot put a Float value into it. The other way around also won’t work: an Int won’t go into a Float.
Even though both types represent numbers of some sort, Swift won’t automatically convert between different number types. You always need to convert the values explicitly.
For example:
var i = 10
var f: Float
f = i // error
f = Float(i) // OK
You don’t always need to specify the type when you create a new variable. If you give the variable an initial value, Swift uses type inference to determine the type:
var i = 10 // Int
var d = 3.14 // Double
var b = true // Bool
var s = "Hello, world" // String
The integer value 10, the floating-point value 3.14, the boolean true and the string "Hello, world" are named literal constants or just literals.
Note that using the value 3.14 in the example above leads Swift to conclude that you want to use a Double here. If you intended to use a Float instead, you’d have to write:
var f: Float = 3.14
The : Float bit is called a type annotation. You use it to override the guess made by Swift’s type inference mechanism, since it doesn’t always get things right.
Likewise, if you wanted the variable i to be a Double instead of an Int, you’d write:
var i: Double = 10
Or a little shorter, by giving the value 10 a decimal point:
var i = 10.0
These simple literals such as 10, 3.14, or "Hello world", are useful only for creating variables of the basic types — Int, Double, String, and so on. To use more complex types, you’ll need to instantiate an object first.
When you write the following,
var item: ChecklistItem
it only tells Swift you want to store a ChecklistItem object into the item variable, but it does not create that ChecklistItem object itself. For that you need to write:
item = ChecklistItem()
This first reserves memory to hold the object’s data, followed by a call to init() to properly set up the object for use. Reserving memory is also called allocation; filling up the object with its initial value(s) is initialization.
The whole process is known as instantiating the object — you’re making an object instance. The instance is the block of memory that holds the values of the object’s variables (that’s why they are called “instance variables,” get it?).
Of course, you can combine the above into a single line:
var item = ChecklistItem()
Here you left out the : ChecklistItem type annotation because Swift is smart enough to realize that the type of item should be ChecklistItem.
However, you can’t leave out the () parentheses — this is how Swift knows that you want to make a new ChecklistItem instance.
Some objects allow you to pass parameters to their init method. For example:
var item = ChecklistItem(text: "Charge my iPhone", checked: false)
This calls the corresponding init(text:checked:) method to prepare the newly allocated ChecklistItem object for usage.
You’ve seen two types of variables: local variables, whose existence is limited to the method they are declared in, and instance variables (also known as “ivars,” or properties) that belong to the object and therefore can be used from within any method in the object.
The lifetime of a variable is called its scope. The scope of a local variable is smaller than that of an instance variable. Once the method ends, any local variables are destroyed.
class MyObject {
var count = 0 // an instance variable
func myMethod() {
var temp: Int // a local variable
temp = count // OK to use the instance variable here
}
// the local variable “temp” doesn’t exist outside the method
}
If you have a local variable with the same name as an instance variable, then it is said to shadow (or hide) the instance variable. You should avoid these situations as they can lead to subtle bugs where you may not be using the variable that you think you are:
class MyObject {
var count = 7 // an instance variable
func myMethod() {
var count = 42 // local variable “hides” instance variable
print(count) // prints 42
}
}
Some developers place an underscore in front of their instance variable names to avoid this problem: _count instead of count. An alternative is to use the keyword self whenever you want to access an instance variable:
func myMethod() {
var count = 42
print(self.count) // prints 7
}
Constants
Variables are not the only code elements that can hold values. A variable is a container for a value that is allowed to change over the course of the app being run.
For example, in a note-taking app, the user can change the text of the note. So, you’d place that text into a String variable. Every time the user edits the text, the variable is updated.
Sometimes, you’ll just want to store the result of a calculation or a method call into a temporary container, after which this value will never change. In that case, it is better to make this container a constant rather than a variable.
The following values cannot change once they’ve been set:
let pi = 3.141592
let difference = abs(targetValue - currentValue)
let message = "You scored \(points) points"
let image = UIImage(named: "SayCheese")
If a constant is local to a method, it’s allowed to give the constant a new value the next time the method is called. The value from the previous method invocation is destroyed when that method ends, and the next time the app enters that method you’re creating a new constant with a new value (but with the same name). Of course, for the duration of that method call, the constant’s value must remain the same.
Tip: My suggestion is to use let for everything — that’s the right solution 90% of the time. When you get it wrong, the Swift compiler will warn that you’re trying to change a constant. Only then should you change it to a var. This ensures you’re not making things variable that don’t need to be.
Value types vs. reference types
When working with basic values such as integers and strings — which are value types — a constant created with let cannot be changed once it has been given a value:
let pi = 3.141592
pi = 3 // not allowed
However, with objects that are reference types, it is only the reference that is constant. The object itself can still be changed:
let item = ChecklistItem()
item.text = "Do the laundry"
item.checked = false
item.dueDate = yesterday
But this is not allowed:
let anotherItem = ChecklistItem()
item = anotherItem // cannot change the reference
So how do you know what is a reference type and what is a value type?
Objects defined as class are reference types, while objects defined as struct or enum are value types. In practice, this means most of the objects from the iOS SDK are reference types but things that are built into the Swift language, such as Int, String, and Array, are value types. (More about this important difference later.)
Collections
A variable stores only a single value. To keep track of multiple objects, you can use a collection object. Naturally, I’m talking about arrays (Array) and dictionaries (Dictionary), both of which you’ve seen previously.
An array stores a list of objects. The objects it contains are ordered sequentially and you retrieve them by index.
// An array of ChecklistItem objects:
var items: Array<ChecklistItem>
// Or, using shorthand notation:
var items: [ChecklistItem]
// Making an instance of the array:
items = [ChecklistItem]()
// Accessing an object from the array:
let item = items[3]
You can write an array as Array<Type> or [Type]. The first one is the official version, the second is “syntactic sugar” that is a bit easier to read. (Unlike other languages, in Swift you don’t write Type[]. The type name goes inside the brackets.)
A dictionary stores key-value pairs. An object, usually a string, is the key that retrieves another object.
// A dictionary that stores (String, Int) pairs, for example a
// list of people’s names and their ages:
var ages: Dictionary<String, Int>
// Or, using shorthand notation:
var ages: [String: Int]
// Making an instance of the dictionary:
ages = [String: Int]()
// Accessing an object from the dictionary:
var age = dict["Jony Ive"]
The notation for retrieving an object from a dictionary looks very similar to reading from an array — both use the [ ] brackets. For indexing an array, you always use a positive integer, but for a dictionary you typically use a string.
There are other sorts of collections as well, but array and dictionary are the most common ones.
Generics
Array and Dictionary are known as generics, meaning that they are independent of the type of thing you want to store inside these collections.
You can have an Array of Int objects, but also an Array of String objects — or an Array of any kind of object, really (even an array of other arrays).
That’s why you have to specify the type of object to store inside the array, before you can use it. In other words, you cannot write this:
var items: Array // error: should be Array<TypeName>
var items: [] // error: should be [TypeName]
There should always be the name of a type inside the [ ] brackets or following the word Array in < > brackets. (If you’re coming from Objective-C, be aware that the < > mean something completely different there.)
For Dictionary, you need to supply two type names: one for the type of the keys and one for the type of the values.
Swift requires that all variables and constants have a value. You can either specify a value when you declare the variable or constant, or by assigning a value inside an init method.
Optionals
Sometimes, it’s useful to have a variable that can have no value, in which case you need to declare it as an optional:
var checklistToEdit: Checklist?
You cannot use this variable immediately; you must always first test whether it has a value or not. This is called unwrapping the optional:
if let checklist = checklistToEdit {
// “checklist” now contains the real object
} else {
// the optional was nil
}
The age variable from the dictionary example in the previous section is actually an optional, because there is no guarantee that the dictionary contains the key “Jony Ive.” Therefore, the type of age is Int? instead of just Int.
Before you can use a value from a dictionary, you need to unwrap it first using if let:
if let age = dict["Jony Ive"] {
// use the value of age
}
If you are 100% sure that the dictionary contains a given key, you can also use force unwrapping to read the corresponding value:
var age = dict["Jony Ive"]!
With the ! you tell Swift, “This value will not be nil. I’ll stake my reputation on it!” Of course, if you’re wrong and the value is nil, the app will crash and your reputation is down the drain. Be careful with force unwrapping!
A slightly safer alternative to force unwrapping is optional chaining. For example, the following will crash the app if the navigationController property is nil:
navigationController!.delegate = self
But this won’t:
navigationController?.delegate = self
Anything after the ? will simply be ignored if navigationController does not have a value. It’s equivalent to writing:
if navigationController != nil {
navigationController!.delegate = self
}
It is also possible to declare an optional using an exclamation point instead of a question mark. This makes it an implicitly unwrapped optional:
var dataModel: DataModel!
Such a value is potentially unsafe because you can use it as a regular variable without having to unwrap it first. If this variable has the value nil when you don’t expect it — and don’t they always — your app will crash.
Optionals exist to guard against such crashes, and using ! undermines the safety of using optionals.
However, sometimes using implicitly unwrapped optionals is more convenient than using pure optionals. Use them when you cannot give the variable an initial value at the time of declaration, nor in init().
But once you’ve given the variable a value, you really ought not to make it nil again. If the value can become nil again, it’s better to use a true optional with a question mark.
Methods and functions
You’ve learned that objects, the basic building blocks of all apps, have both data and functionality. Instance variables and constants provide the data, methods provide the functionality.
When you call a method, the app jumps to that section of the code and executes all the statements in the method one-by-one. When the end of the method is reached, the app jumps back to where it left off:
let result = performUselessCalculation(314)
print(result)
. . .
func performUselessCalculation(_ a: Int) -> Int {
var b = Int(arc4random_uniform(100))
var c = a / 2
return (a + b) * c
}
Methods often return a value to the caller, usually the result of a computation or looking up something in a collection. The data type of the result value is written after the -> arrow. In the example above, it is Int. If there is no -> arrow, the method does not return a value (also known as returning Void).
Methods are functions that belong to an object, but there are also standalone functions such as print().
Functions serve the same purpose as methods — they bundle functionality into small re-usable units — but live outside of any objects. Such functions are also called free functions or global functions.
These are examples of methods:
// Method with no parameters, no return a value.
override func viewDidLoad()
// Method with one parameter, slider. No return a value.
// The keyword @IBAction means that this method can be connected
// to a control in Interface Builder.
@IBAction func sliderMoved(_ slider: UISlider)
// Method with no parameters, returns an Int value.
func countUncheckedItems() -> Int
// Method with two parameters, cell and item, no return value.
// Note that the first parameter has an extra label, for,
// and the second parameter has an extra label, with.
func configureCheckmarkFor(for cell: UITableViewCell,
with item: ChecklistItem)
// Method with two parameters, tableView and section.
// Returns an Int. The _ means the first parameter does not
// have an external label.
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
// Method with two parameters, tableView and indexPath.
// The question mark means it returns an optional IndexPath
// object (may also return nil).
override func tableView(_ tableView: UITableView,
willSelectRowAt indexPath: IndexPath) -> IndexPath?
To call a method on an object, you write object.method(parameters). For example:
// Calling a method on the lists object:
lists.append(checklist)
// Calling a method with more than one parameter:
tableView.insertRows(at: indexPaths, with: .fade)
You can think of calling a method as sending a message from one object to another: “Hey lists, I’m sending you the append message for this checklist object.”
The object whose method you’re calling is known as the receiver of the message.
It is very common to call a method from the same object. Here, loadChecklists() calls the sortChecklists() method. Both are members of the DataModel object.
class DataModel {
func loadChecklists() {
. . .
sortChecklists() // this method also lives in DataModel
}
func sortChecklists() {
. . .
}
}
Sometimes, this is written as:
func loadChecklists() {
. . .
self.sortChecklists()
}
The self keyword makes it clear that the DataModel object itself is the receiver of this message.
Note: In this book, the
selfkeyword is left out for method calls, because it’s not necessary to have it. Objective-C developers are very attached toself, so you’ll probably see it used a lot in Swift too. It is a topic of heated debate in developer circles, but except for a few specific scenarios, the compiler doesn’t really care whether you useselfor not.
Inside a method, you can also use self to get a reference to the object itself:
@IBAction func cancel() {
delegate?.itemDetailViewControllerDidCancel(self)
}
Here, cancel() sends a reference to the object (i.e. self) along to the delegate, so the delegate knows who sent this itemDetailViewControllerDidCancel() message.
Also note that the use of optional chaining here. The delegate property is an optional, so it can be nil. Using the question mark before the method call will ensure nothing bad happens if delegate is not set.
Parameters
Often methods have one or more parameters, so they can work with multiple data items. A method that is limited to a fixed set of data is not very useful or reusable. Consider sumValuesFromArray(), a method that has no parameters:
class MyObject {
var numbers = [Int]()
func sumValuesFromArray() -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
}
Here, numbers is an instance variable. The sumValuesFromArray() method is tied closely to that instance variable, and is useless without it.
Suppose you add a second array to the app that you also want to apply this calculation to. One approach is to copy-paste the above method and change the name of the variable to that of the new array. That certainly works, but it’s not smart programming!
It is better to give the method a parameter that allows you to pass in the array object that you wish to examine. Then, the method becomes independent from any instance variables:
func sumValues(from array: [Int]) -> Int {
var total = 0
for number in array {
total += number
}
return total
}
Now you can call this method with any [Int] (or Array<Int>) object as its parameter.
This doesn’t mean methods should never use instance variables, but if you can make a method more general by giving it a parameter, then that is usually a good idea.
Often methods use two names for their parameters, the external label and the internal label. For example:
func downloadImage(for searchResult: SearchResult,
withTimeout timeout: TimeInterval,
andPlaceOn button: UIButton) {
. . .
}
This method has three parameters: searchResult, timeout, and button. Those are the internal parameter names you’d use in the code inside the method.
The external labels become part of the method name. The full name for the method is downloadImage(for:withTimeout:andPlaceOn:) — method names in Swift are often quite long!
To call this method, you’d use the external labels:
downloadImage(for: result, withTimeout: 10,
andPlaceOn: imageButton)
Sometimes you’ll see a method whose first parameter does not have an external label, but has an _ underscore instead:
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
This is often the case with delegate methods. It’s a holdover from the Objective-C days, where the label for the first parameter was embedded in the first part of the method name. For example, in Objective-C the downloadImage() method example above would be named downloadImageForSearchResult(). These kinds of names should become less and less common in the near future.
Swift is pretty flexible with how it lets you name your methods, but it’s smart to stick to the established conventions.
Inside a method you can do the following things:
- Create local variables and constants.
- Do basic arithmetic with mathematical operators such as
+,-,*,/, and%. - Put new values into variables (both local and instance variables).
- Call other methods.
- Make decisions with
iforswitchstatements. - Perform repetitions with the
fororwhilestatements. - Return a value to the caller.
Let’s look at the if and for statements in more detail.
Making decisions
The if statement looks like this:
if count == 0 {
text = "No Items"
} else if count == 1 {
text = "1 Item"
} else {
text = "\(count) Items"
}
The expression after if is called the condition. If a condition is true then the statements in the following { } block are executed. The else section gets performed if none of the conditions are true.
Comparison Operators
You use comparison operators to perform comparisons between two values:
== equal to
!= not equal
> greater than
>= greater than or equal
< less than
<= less than or equal
let a = "Hello, world"
let b = "Hello," + " world"
print(a == b) // prints true
When you use the == operator, the contents of the objects are compared. The above code only returns true if a and b have the same value:
This is different from Objective-C, where == is only true if the two objects are the exact same instance in memory. However, in Swift == compares the values of the objects, not whether they actually occupy the same spot in memory. (If you need to do that use ===, the identity operator.)
Logical Operators
You can use logical operators to combine two expressions:
a && b is true if both a and b are true
a || b is true when either a or b is true (or both)
There is also the logical not operator, !, that turns true into false, and false into true. (Don’t confuse this with the ! that is used with optionals.)
You can group expressions with ( ) parentheses:
if ((this && that) || (such && so)) && !other {
// statements
}
This reads as:
if ((this and that) or (such and so)) and not other {
// statements
}
Or if you want to see clearly in which order these operations are performed:
if (
(this and that)
or
(such and so)
)
and
(not other)
Of course, the more complicated you make it, the harder it is to remember exactly what you’re doing!
switch statement
Swift has another very powerful construct in the language for making decisions, the switch statement:
switch condition {
case value1:
// statements
case value2:
// statements
case value3:
// statements
default:
// statements
}
It works the same way as an if statement with a bunch of else ifs. The following is equivalent:
if condition == value1 {
// statements
} else if condition == value2 {
// statements
} else if condition == value3 {
// statements
} else {
// statements
}
In such a situation, the switch statement would be more convenient to use. Swift’s version of switch is much more powerful than the one in Objective-C. For example, you can match on ranges and other patterns:
switch difference {
case 0:
title = "Perfect!"
case 1..<5:
title = "You almost had it!"
case 5..<10:
title = "Pretty good!"
default:
title = "Not even close..."
}
The ..< is the half-open range operator. It creates a range between the two numbers, but the top number is exclusive. So the half-open range 1..<5 is the same as the closed range 1...4.
You’ll see the switch statement in action a little later on.
return statement
Note that if and return can be used to return early from a method:
func divide(_ a: Int, by b: Int) -> Int {
if b == 0 {
print("You really shouldn’t divide by zero")
return 0
}
return a / b
}
This can even be done for methods that don’t return a value:
func performDifficultCalculation(list: [Double]) {
if list.count < 2 {
print("Too few items in list")
return
}
// perform the very difficult calculation here
}
In this case, return simply means: “We’re done with the method.” Any statements following the return are skipped and execution immediately returns to the caller.
You could also have written it like this:
func performDifficultCalculation(list: [Double]) {
if list.count < 2 {
print("Too few items in list")
} else {
// perform the very difficult calculation here
}
}
Which approach you use is up to you. The advantage of an early return is that it avoids multiple nested blocks of code with multiple levels of indentation — the code just looks cleaner.
For example, sometimes you see code like this:
func someMethod() {
if condition1 {
if condition2 {
if condition3 {
// statements
} else {
// statements
}
} else {
// statements
}
} else {
// statements
}
}
This can become very hard to read. You could restructure that kind of code as follows:
func someMethod() {
if !condition1 {
// statements
return
}
if !condition2 {
// statements
return
}
if !condition3 {
// statements
return
}
// statements
}
Both do exactly the same thing, but the second one is easier to understand. (Note that the conditions now use the ! operator to invert their meaning.)
Swift even has a dedicated feature, guard, to help write this kind of code. It looks like this:
func someMethod() {
guard condition1 else {
// statements
return
}
guard condition2 else {
// statements
return
}
. . .
As you become more experienced, you’ll start to develop your own taste for what looks good and what is readable code.
Loops
You’ve seen the for...in statement for looping through an array:
for item in items {
if !item.checked {
count += 1
}
}
Which can also be written as:
for item in items where !item.checked {
count += 1
}
This performs the statements inside the for...in block once for each object from the items array matching the condition provided by the where clause.
Note that the scope of the variable item is limited to just this for statement. You can’t use it outside this statement, so its lifetime is even shorter than a local variable.
Looping through number ranges
Some languages, including Swift 2, have a for statement that looks like this:
for var i = 0; i < 5; ++i {
print(i)
}
When you run this code, it should print:
0
1
2
3
4
However, as of Swift 3.0 this kind of for loop was removed from the language. Instead, you can loop over a range. This has the same output as above:
for i in 0...4 { // or 0..<5
print(i)
}
By the way, you can also write this loop as:
for i in stride(from: 0, to: 5, by: 1) {
print(i)
}
The stride() function creates a special object that represents the range 0 to 5 in increments of 1. If you wanted to show just the even numbers, you could change the by parameter to 2. You can even use stride() to count backwards if you pass the by parameter a negative number.
while statement
The for statement is not the only way to perform loops. Another very useful looping construct is the while statement:
while something is true {
// statements
}
The while loop keeps repeating the statements until its condition becomes false. You can also write it as follows:
repeat {
// statements
} while something is true
In the latter case, the condition is evaluated after the statements have been executed at least once.
You can rewrite the loop that counts the ChecklistItems as follows using a while statement:
var count = 0
var i = 0
while i < items.count {
let item = items[i]
if !item.checked {
count += 1
}
i += 1
}
Most of these looping constructs are really the same, they just look different. Each of them lets you repeat a bunch of statements until some ending condition is met.
Still, using a while is slightly more cumbersome than “for item in items,” which is why you’ll see for...in used most of the time.
There really is no significant difference between using a for, while, or repeat...while loop, except that one may be easier to read than the others, depending on what you’re trying to do.
Note:
items.countandcountin this example are two different things with the same name. The firstcountis a property on theitemsarray that returns the number of elements in that array; the secondcountis a local variable that contains the number of unchecked to-do items counted so far.
Just like you can prematurely exit from a method using the return statement, you can exit a loop at any time using the break statement:
var found = false
for item in array {
if item == searchText {
found = true
break
}
}
This example loops through the array until it finds an item that is equal to the value of searchText (presumably both are strings). Then it sets the variable found to true and jumps out of the loop using break. You’ve found what you were looking for, so it makes no sense to look at the other objects in that array — for all you know there could be hundreds of items.
There is also a continue statement that is somewhat the opposite of break. It doesn’t exit the loop but immediately skips to the next iteration. You use continue to say, “I’m done with the current item, let’s look at the next one.”
Loops can often be replaced by functional programming constructs such as map, filter, or reduce. These are known as higher order functions and they operate on a collection, performing some code for each element, and return a new collection (or single value, in the case of reduce) with the results.
For example, using filter on an array will return items that satisfy a certain condition. To get a list of all the unchecked ChecklistItem objects, you’d write:
var uncheckedItems = items.filter { item in !item.checked }
That’s a lot simpler than writing a loop. Functional programming is an advanced topic so we won’t spend too much time on it here.
Objects
Objects are what it’s all about. They combine data with functionality into coherent, reusable units — that is, if you write them properly!
The data is made up of the object’s instance variables and constants. We often refer to these as the object’s properties. The functionality is provided by the object’s methods.
In your Swift programs you will use existing objects, such as String, Array, Date, UITableView, and you’ll also make your own.
To define a new object, you need a bit of code that contains a class section:
class MyObject {
var text: String
var count = 0
let maximum = 100
init() {
text = "Hello world"
}
func doSomething() {
// statements
}
}
Inside the brackets for the class, you add properties (the instance variables and constants) and methods.
Properties
There are two types of properties:
- Stored properties are the usual instance variables and constants.
- Computed properties don’t store a value, but perform logic when you read from, or write to, their values.
This is an example of a computed property:
var indexOfSelectedChecklist: Int {
get {
return UserDefaults.standard.integer(
forKey: "ChecklistIndex")
}
set {
UserDefaults.standard.set(newValue,
forKey: "ChecklistIndex")
}
}
The indexOfSelectedChecklist property does not store a value like a normal variable would. Instead, every time someone uses this property, it performs the code from the get or set block.
The alternative would be to write separate setIndexOfSelectedChecklist() and getIndexOfSelectedChecklist() methods, but that doesn’t read as nicely.
If a property name is preceded by the keyword @IBOutlet, that means that the property can refer to a user interface element in Interface Builder, such as a label or button. Such properties are usually declared weak and optional.
Similarly, the keyword @IBAction is used for methods that will be performed when the user interacts with the app.
Methods
There are three kinds of methods:
- Instance methods
- Class methods
- Init methods
As mentioned previously, a method is a function that belongs to an object. To call such a method you first need to have an instance of the object:
let myInstance = MyObject() // create the object instance
. . .
myInstance.doSomething() // call the method
You can also have class methods, which can be used without an object instance. In fact, they are often used as “factory” methods, to create new object instances:
class MyObject {
. . .
class func makeObject(text: String) -> MyObject {
let m = MyObject()
m.text = text
return m
}
}
let myInstance = MyObject.makeObject(text: "Hello world")
Init methods, or initializers, are used during the creation of new object instances. Instead of the above factory method, you might as well use a custom init method:
class MyObject {
. . .
init(text: String) {
self.text = text
}
}
let myInstance = MyObject(text: "Hello world")
The main purpose of an init method is to set up (or, initialize) the object’s properties. Any instance variables or constants that do not have a value yet must be given one in the init method.
Swift does not allow variables or constants to have no value (except for optionals), and init is your last chance to make this happen.
Objects can have more than one init method; which one you use depends on the circumstances.
A UITableViewController, for example, can be initialized either with init?(coder:) when automatically loaded from a storyboard, with init(nibName:bundle:) when manually loaded from a nib file, or with init(style:) when constructed without a storyboard or nib — sometimes you use one, sometimes the other. You can also provide a deinit method that gets called just before the object is destroyed.
By the way, class isn’t the only way to define an object in Swift. It also supports other types of objects such as structs and enums. You’ll learn more about these later in the book.
Protocols
Besides objects, you can also define protocols. A protocol is simply a list of method names (and possibly, properties):
protocol MyProtocol {
func someMethod(value: Int)
func anotherMethod() -> String
}
A protocol is like a job ad. It lists all the things that a candidate for a certain position in your company should be able to do.
But the ad itself doesn’t do the job — it’s just words printed in the careers section of the newspaper. So, you need to hire an actual employee who can get the job done. That would be an object.
Objects need to indicate that they conform to a protocol:
class MyObject: MyProtocol {
. . .
}
This object now has to provide an implementation for the methods listed in the protocol. (If not, it’s fired!)
From then on, you can refer to this object as a MyObject (because that is its class name) but also as a MyProtocol object:
var m1: MyObject = MyObject()
var m2: MyProtocol = MyObject()
To any part of the code using the m2 variable, it doesn’t matter that the object is really a MyObject under the hood. The type of m2 is MyProtocol, not MyObject.
All your code sees is that m2 is some object conforming to MyProtocol, but it’s not important what sort of object that is.
In other words, you don’t really care that your employee may also have another job on the side, as long as it doesn’t interfere with the duties you’ve hired him, or her, for.
Protocols are often used to define delegates, but they come in handy for other uses as well, as you’ll find out later on.
This concludes the quick recap of what you’ve seen so far of the Swift language. After all that theory, it’s time to write some code!