13.
Delegates & Protocols
Written by Fahim Farook
You now have an Add Item screen showing a keyboard that lets the user enter text. The app also properly validates the input so that you’ll never end up with text that is empty.
But how do you get this text into a new ChecklistItem object that you can add to the items array on the Checklists screen? That is the topic that this chapter will explore.
Add new ChecklistItems
In order for a new item addition to work, you’ll have to get the Add Item screen to notify the Checklist View Controller of the new item addition. This is one of the fundamental tasks that every iOS app needs to do: sending messages from one view controller to another.
The messy way
Exercise: How would you tackle this problem? The
done()method needs to create a newChecklistItemobject with the text from the text field (easy), then add it to theitemsarray and the table view inChecklistViewController(not so easy).
Maybe you came up with something like this:
class AddItemViewController: UITableViewController, . . . {
// This variable refers to the other view controller
var checklistViewController: ChecklistViewController
@IBAction func done() {
// Create the new checklist item object
let item = ChecklistItem()
item.text = textField.text!
// Directly call a method from ChecklistViewController
checklistViewController.add(item)
}
}
In this scenario, AddItemViewController has a variable that refers to the ChecklistViewController, and done() calls its add() method with the new ChecklistItem object.
This will work, but it’s not the iOS way. The big downside to this approach is that it shackles these two view controller objects together.
As a general principle, if screen A launches screen B then you don’t want screen B to know too much about the screen that invoked it (A). The less B knows of A, the better.
Giving AddItemViewController a direct reference to ChecklistViewController prevents you from opening the Add Item screen from somewhere else in the app. It can only ever talk back to ChecklistViewController. That’s a big disadvantage.
You won’t actually need to do this in Checklists, but in many apps it’s common for one screen to be accessible from multiple places. For example, a login screen that appears after the user has been logged out due to inactivity. Or, a details screen that shows more information about a tapped item, no matter where that item is located in the app — you’ll see an example of this in the next app.
Therefore, it’s best if AddItemViewController doesn’t know anything about ChecklistViewController.
But if that’s the case, then how can you make the two communicate?
The solution is to make your own delegate.
The delegate way
You’ve already seen delegates in a few different places: the table view has a delegate that responds to taps on the rows; the text field has a delegate that you used to validate the length of the text; and the app also has things named AppDelegate and SceneDelegate — check the project navigator.
You can’t turn a corner in this place without bumping into a delegate…
The delegate pattern is commonly used to handle the situation you find yourself in: Screen A opens screen B. At some point screen B needs to communicate back to screen A, usually when it closes.
The solution is to make screen A the delegate of screen B, so that B can send its messages to A whenever it needs to.
The cool thing about the delegate pattern is that screen B doesn’t really know anything about screen A. It just knows that some object is its delegate, but doesn’t really care who that is. Just like how UITableView doesn’t really care about your view controller, only that it delivers table view cells when the table view asks for them.
This principle, where screen B is independent of screen A and yet can still talk to it, is called loose coupling and is considered good software design practice.
You will use the delegate pattern to let the AddItemViewController send notifications back to the ChecklistViewController without it having to know anything about the latter.
Delegates go hand-in-hand with protocols, a prominent feature of the Swift language.
The delegate protocol
➤ At the top of AddItemViewController.swift, add the following after the import line, but before the class line — it is not part of the AddItemViewController object:
protocol AddItemViewControllerDelegate: AnyObject {
func addItemViewControllerDidCancel(
_ controller: AddItemViewController)
func addItemViewController(
_ controller: AddItemViewController,
didFinishAdding item: ChecklistItem
)
}
This defines the AddItemViewControllerDelegate protocol. You should recognize the lines inside the protocol { ... } block as method declarations, but unlike the previous methods you’ve seen, these don’t have any source code in them. The protocol just lists the names of the methods.
Think of the delegate protocol as a contract between screen B — in this case the Add Item View Controller — and any screens that wish to use it.
And if you’re wondering why you have the keyword AnyObject after the colon in the protocol name, let me explain. You might have noticed that the syntax for the protocol declaration looks very similar to how you’d declared classes previously, giving the name of our class followed by a colon and then specifying the class our class inherited from.
This is exactly the same thing here with protocols, you can have one protocol inherit from another protocol. But you can also specify a particular type of object which can adopt your protocol. The AnyObject keyword identifies that we want the AddItemViewControllerDelegate protocol to be limited to all class types.
And if you ask me at this point, “Why is that?”, I can give you a fairly long-winded explanation about why this is so, but it all boils down to the fact that we mark any references to this protocol as weak and in order to have weak references, we need a protocol which can only be used with a reference type.
We’ll discuss weak references just a bit further along in this chapter and it might become a little bit more clearer at that point. :]
Protocols
In Swift, a protocol doesn’t have anything to do with computer networks or meeting royalty. It is simply a name for a group of methods.
A protocol normally doesn’t implement any of the methods it declares. It just says: any object that conforms to this protocol must implement methods X, Y and Z. There are special cases where you might want to provide a default implementation for a protocol, but that’s an advanced topic that we don’t need to get into right now :]
The two methods listed in the AddItemViewControllerDelegate protocol are:
addItemViewControllerDidCancel(_:)addItemViewController(_:didFinishAdding:)
Delegates often have very long method names!
The first method is for when the user presses Cancel, the second is for when they press Done. In the latter case, the didFinishAdding parameter passes along the new ChecklistItem object.
To make the ChecklistViewController conform to this protocol, it must provide implementations for these two methods. From then on, you can refer to ChecklistViewController using the protocol name, instead of the class name.
If you’ve programmed in other languages before, you may recognize protocols as being very similar to “interfaces”.
In AddItemViewController, you can use the following to refer back to ChecklistViewController — but don’t add this line just yet:
var delegate: AddItemViewControllerDelegate
The variable delegate is nothing more than a reference to some object that implements the methods of the AddItemViewControllerDelegate protocol. You can send messages to the object referenced by the delegate variable without knowing what kind of object it really is.
Of course, you know the object referenced by delegate is the ChecklistViewController, but AddItemViewController doesn’t need to be aware of that. All it sees is some object that implements its delegate protocol.
If you wanted to, you could make some other object implement the protocol and AddItemViewController would be perfectly OK with that. That’s the power of delegation: you have removed – or abstracted away – the dependency between the AddItemViewController and the rest of the app.
It may seem a little overkill for a simple app such as this, but delegates are one of the cornerstones of iOS development. The sooner you master them, the better!
Notify the delegate
You’re not done yet in AddItemViewController.swift. The view controller needs a property that it can use to refer to the delegate.
➤ Add this inside the AddItemViewController class, below the outlets:
weak var delegate: AddItemViewControllerDelegate?
It looks like a regular instance variable declaration, with two differences: weak and the question mark.
Delegates are usually declared as being weak – not a statement of their moral character but a way to describe the relationship between the view controller and its delegate. Delegates are also optional (the question mark — which you learnt a bit about in the previous chapter).
You’ll learn more about those things in a moment.
➤ Replace the cancel() and done() actions with the following:
@IBAction func cancel() {
delegate?.addItemViewControllerDidCancel(self)
}
@IBAction func done() {
let item = ChecklistItem()
item.text = textField.text!
delegate?.addItemViewController(self, didFinishAdding: item)
}
Let’s look at the changes you made. When the user taps the Cancel button, you send the addItemViewControllerDidCancel(_:) message back to the delegate.
You do something similar for the Done button, except that the message is addItemViewController(_:didFinishAdding:) and you pass along a new ChecklistItem object that has the text string from the text field.
Note: It is customary for the delegate methods to have a reference to their owner as the first (or only) parameter.
Doing this is not required, but still a good idea. For example, in the case of table views, it may happen that an object is the delegate or data source for more than one table view. In that case, you need to be able to distinguish between those table views. To allow for this, the table view delegate methods have a parameter for the
UITableViewobject that sent the notification. Having this reference also saves you from having to make an@IBOutletfor the table view.That explains why you pass
selfto your delegate methods. Recall thatselfrefers to the object itself, in this caseAddItemViewController. It’s also why all the delegate method names start withaddItemViewController.
➤ Run the app and try the Cancel and Done buttons. They no longer work!
I hope you’re not too surprised… The Add Item screen now depends on a delegate to make it close, but you haven’t told the Add Item screen who its delegate is yet.
That means the delegate property has no value and the messages aren’t being sent to anyone – there is no one listening for them.
Optionals
I mentioned a few times that variables and constants in Swift must always have a value. In other programming languages the special symbol nil or NULL is often used to indicate that a variable has no value. This is not allowed in Swift for normal variables.
The problem with nil and NULL is that they are a frequent cause of crashing apps. If an app attempts to use a variable that is nil when you don’t expect it to be nil, the app will crash. This is the dreaded “null pointer dereference” error.
Swift stops this by preventing you from using nil with regular variables.
However, sometimes a variable does need to have “no value”. In that case you can make it an optional. You mark something as optional in Swift using either a question mark ? or an exclamation point !.
Only variables that are made optional can have the value nil.
You’ve already seen the question mark used with IndexPath?, the return type of tableView(_:willSelectRowAt:). Returning nil from this method is a valid response; it means that the table should not select a particular row.
The question mark tells Swift that it’s OK for the method to return nil instead of an actual IndexPath object.
Variables that refer to a delegate are usually marked as optional too. You can tell because there is a question mark behind the type:
weak var delegate: AddItemViewControllerDelegate?
Thanks to the ? it’s perfectly acceptable for a delegate to be nil.
You may be wondering why the delegate would ever be nil. Doesn’t that negate the idea of having a delegate in the first place? There are two reasons.
Often, delegates are truly optional; a UITableView works fine even if you don’t implement any of its delegate methods (but you do need to provide at least some of its data source methods).
More importantly, when AddItemViewController is loaded from the storyboard and instantiated, it won’t know right away who its delegate is. Between the time the view controller is loaded and the delegate is assigned, the delegate variable will be nil. And variables that can be nil, even if it is only temporary, must be optionals.
When delegate is nil, you don’t want cancel() or done() to send any of the messages. Doing that would crash the app because there is no one to receive the messages.
Swift has a handy shorthand for skipping the work when delegate is not set:
delegate?.addItemViewControllerDidCancel(self)
Here the ? tells Swift not to send the message if delegate is nil. You can read this as, “Is there a delegate? Then send the message.” This practice is called optional chaining and it’s used a lot in Swift.
In this app it should never happen that delegate is nil – that would get users stuck on the Add Item screen. But Swift doesn’t know that. So you’ll have to pretend that it can happen anyway and use optional chaining to send messages to the delegate.
Optionals aren’t common in other programming languages, so they may take some getting used to. I find that optionals do make programs clearer – most variables never have to be nil, so it’s good to prevent them from becoming nil and avoid these potential sources of bugs.
Remember, if you see ? or ! in a Swift program, you’re dealing with optionals. In the course of this app I’ll come back to this topic a few more times and explain the finer points of using optionals in more detail.
Conform to the delegate protocol
Before you can give AddItemViewController its delegate, you first need to make the ChecklistViewController suitable to play the role of delegate.
➤ In ChecklistViewController.swift, change the class line to the following (this goes all on one line):
class ChecklistViewController: UITableViewController, AddItemViewControllerDelegate {
This tells the compiler that ChecklistViewController now promises to do the things from the AddItemViewControllerDelegate protocol. Or, in programming terminology, that it conforms to the AddItemViewControllerDelegate protocol.
Xcode should now throw up an error: “Type ChecklistViewController does not conform to protocol AddItemViewControllerDelegate.”
That is correct: You still need to add the methods that are listed in AddItemViewControllerDelegate. In Xcode there is an easy way to get started with fixing this issue — see that “Fix” button? Simply click it. :]
Xcode will add in the stubs — the bare minimum code — for the missing methods. You will have to add in the actual implementation for each method, of course.
➤ Add the implementations for the protocol methods to ChecklistViewController:
// MARK: - Add Item ViewController Delegates
func addItemViewControllerDidCancel(
_ controller: AddItemViewController
) {
navigationController?.popViewController(animated: true)
}
func addItemViewController(
_ controller: AddItemViewController,
didFinishAdding item: ChecklistItem
) {
navigationController?.popViewController(animated: true)
}
Currently, both methods simply close the Add Item screen. This is what the AddItemViewController used to do in its cancel() and done() actions. You’ve simply moved that responsibility to the delegate.
The code that adds the new ChecklistItem object to the table view is yet to be added. You’ll do that in a moment, but there’s something else you need to do first.
Delegates in five easy steps
These are the steps for setting up the delegate pattern between two objects, where object A is the delegate for object B, and object B will send messages back to A. The steps are:
-
Define a delegate
protocolfor object B. -
Give object B an optional
delegatevariable. This variable should beweak. -
Update object B to send messages to its delegate when something interesting happens, such as the user pressing the Cancel or Done buttons, or when it needs a piece of information. You write
delegate?.methodName(self, . . .) -
Make object A conform to the delegate protocol. It should put the name of the protocol in its
classline and implement the methods from the protocol. -
Tell object B that object A is now its delegate.
You’ve done steps 1 - 4, so there is just one more thing you need to do — step 5: tell AddItemViewController that ChecklistViewController is its delegate.
The proper place to do that is in the prepare(for:sender:) method, also known as prepare-for-segue.
The prepare(for:sender:) method is invoked by UIKit when a segue from one screen to another is about to be performed. Recall that the segue is the arrow between two view controllers in the storyboard.
Using prepare-for-segue allows you to pass data to the new view controller before it is displayed. Usually you’ll do this by setting one or more of the new view controller’s properties.
➤ Add this method to ChecklistViewController.swift:
// MARK: - Navigation
override func prepare(
for segue: UIStoryboardSegue,
sender: Any?
) {
// 1
if segue.identifier == "AddItem" {
// 2
let controller = segue.destination as! AddItemViewController
// 3
controller.delegate = self
}
}
This is what this code does, step-by-step:
-
Because there may be more than one segue per view controller, it’s a good idea to give each segue a unique identifier and to check for that identifier first to make sure you’re handling the correct segue. Swift’s
==comparison operator works on not just numbers but also on strings and some other types of objects. -
The new view controller to be displayed can be found in
segue.destination, butdestinationis of typeUIViewContollersince the new view controller could be any view controller sub-class.So, you cast
destinationtoAddItemViewControllerto get a reference to an object with the right type. (Theas!keyword is known as a type cast or a force downcast since you are casting an object of one type to a different type. Do note that if you downcast objects of completely different types, you might get anilvalue. The casting works here becauseAddItemViewControlleris a sub-class ofUIViewContoller.) -
Once you have a reference to the
AddItemViewControllerobject, you set itsdelegateproperty toselfand the connection is complete. This tellsAddItemViewControllerthat from now on, the object identified asselfis its delegate. But what is “self” here? Well, since you’re editing ChecklistViewController.swift,selfrefers toChecklistViewController.
Excellent! ChecklistViewController is now the delegate of AddItemViewController. It took some work, but you’re almost set now — except for one teensy thing :]
Set the segue identifier
See the segue identifier mentioned in the code above? Where was it set? The answer is, that it wasn’t! We need to set the identifier in order for the above code to work. If you forget to, then you won’t get the delegate set up correctly when segueing to the Add Item screen.
➤ Open the storyboard and select the segue between the Checklist View Controller and the Add Item View Controller.
➤ In the Attributes inspector, type AddItem into the Identifier field:
➤ Run the app to see if it works — make sure the storyboard is saved before you press Run, or the app may crash or not work correctly.
Pressing the + button will perform the segue to the Add Item screen with the Checklists screen set as its delegate.
When you press Cancel or Done, AddItemViewController sends a message to its delegate, ChecklistViewController. Currently the delegate simply closes the Add Item screen. But now that you know it works, you can make it do more.
Let’s add the new ChecklistItem to the data model and the table view. Finally!
Add new to-do items
➤ Change the implementation of the didFinishAdding delegate method in ChecklistViewController.swift to the following:
func addItemViewController(
_ controller: AddItemViewController,
didFinishAdding item: ChecklistItem
) {
let newRowIndex = items.count
items.append(item)
let indexPath = IndexPath(row: newRowIndex, section: 0)
let indexPaths = [indexPath]
tableView.insertRows(at: indexPaths, with: .automatic)
navigationController?.popViewController(animated:true)
}
This is basically the same as what you did in addItem() before. In fact, I simply copied the contents of addItem() and pasted that into this method with some slight modifications. Compare the two methods and see for yourself.
The only difference is that you no longer create the ChecklistItem object here; that happens in the AddItemViewController. You merely insert this new object into the items array.
As before, you tell the table view you have a new row for it and then close the Add Items screen.
➤ Remove addItem() from ChecklistViewController.swift as you no longer need this method.
Just to make sure, open the storyboard and double-check that the + button is no longer connected to the addItem action. You should have already removed the connection to the action when you set up the segue to the Add Items scene, but it doesn’t hurt to check since bad things happen if buttons are connected to methods that no longer exist…
You can check this in the Connections inspector for the + button, under Sent Actions. Nothing should be connected there. Only the segue under Triggered Segues should be present.
➤ Run the app and you should be able to add your own items to the list!
Weak
I still owe you an explanation about the weak keyword. Relationships between objects can be weak or strong. You use weak relationships to avoid what is known as an ownership cycle.
When object A has a strong reference to object B, and at the same time object B also has a strong reference back to A, then these two objects are involved in a dangerous kind of romance: an ownership cycle.
Normally, an object is destroyed – or deallocated – when there are no more strong references to it. But because A and B have strong references to each other, they keep each other alive.
The result is a potential memory leak where an object that ought to be destroyed, isn’t, and the memory for its data is never reclaimed. With enough such leaks, iOS will run out of available memory and your app will crash. I told you it was dangerous!
Due to the strong references between them, A owns B and at the same time, B also owns A:
To avoid ownership cycles you can make one of these references weak.
In the case of a view controller and its delegate, screen A usually has a strong reference to screen B, but B only has a weak reference back to its delegate, A.
Because of the weak reference, B no longer owns A:
Now there is no ownership cycle.
Such cycles can occur in other situations too, but they are most common with delegates. Therefore, delegates are always made weak.
(There is another relationship type, unowned, that is similar to weak and can be used for delegates too. The difference is that weak variables are allowed to become nil again. You may forget this right now.)
@IBOutlets are usually also declared with the weak keyword. This isn’t done to avoid an ownership cycle, but to make it clear that the view controller isn’t really the owner of the views from the outlets.
In the course of this book, you’ll learn more about weak, strong, optionals, and the relationships between objects. These are important concepts in Swift, but they may take a while to make sense. If you don’t understand them immediately, don’t lose any sleep over it!
You can find the project files for the app up to this point under 13-Delegates-and-protocols in the Source Code folder.