24.
Delegates & Protocols
Written by Eli Ganim
You now have an Edit High Score 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’s empty.
But how do you get this text into HighScoreItem and add it to items on the High Scores screen? That’s the topic that this chapter will explore.
Updating HighScoreItem
For editing to work, you’ll have to get the Edit High Score screen to notify the High Scores View Controller of the updated HighScoreItem. This is one of the fundamental tasks that every iOS app needs to do: Send messages from one view controller to another.
The messy way
Exercise: How would you tackle this problem?
done()needs to updateHighScoreItemwith the text from the text field, which is easy, then update it in the Table view inHighScoreViewController, which is not so easy.
Maybe you came up with something like this:
class EditHighScoreViewController: UITableViewController, . . . {
// This variable refers to the other view controller
var highScoresViewController: HighScoresViewController
@IBAction func done() {
highScoreItem.name = textField.text!
// Directly call a method from HighScoresViewController
highScoresViewController.update(item)
}
}
In this scenario, EditHighScoreViewController has a variable that refers to the HighScoresViewController, and done() calls its update() method with the new HighScoreItem. 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 EditHighScoreViewController a direct reference to HighScoresViewController prevents you from opening the Edit High Score screen from somewhere else in the app. It can only ever talk back to HighScoresViewController. That’s a big disadvantage.
You won’t need to do this in Bullseye, but in many apps, it’s common for one screen to be accessible from multiple places. Examples include a login screen that appears after the app has logged a user out for inactivity, or a details screen that shows more information about a tapped item no matter where that item is in the app. You’ll see an example of this in the next app.
Therefore, it’s best if EditHighScoreViewController doesn’t know anything about HighScoresViewController. But if that’s the case, 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. The app also has something named the AppDelegate (see the project navigator).
It seems like 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 knows that some object is its delegate, but it 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 good software design practice.
You’ll use the delegate pattern to let the EditHighScoreViewController send notifications back to the HighScoresViewController 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 EditHighScoreViewController.swift, add the following after the import line but before the class line — it’s not part of the EditHighScoreViewController object:
protocol EditHighScoreViewControllerDelegate: class {
func editHighScoreViewControllerDidCancel(
_ controller: EditHighScoreViewController)
func editHighScoreViewController(
_ controller: EditHighScoreViewController,
didFinishEditing item: HighScoreItem)
}
This defines EditHighScoreViewControllerDelegate. You should recognize that the lines inside protocol { ... } block as method declarations, but unlike the previous methods you’ve seen, they 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, or the Edit High Score View Controller in this case, and any screens that wish to use it.
Are you wondering why you have the keyword class after the colon in the protocol name? You might have noticed that the syntax for the protocol declaration looks similar to the one you’ve used to declare classes previously, giving the name of your class followed by a colon and then specifying the class your class inherited from.
You’re seeing 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 to adopt your protocol. The class keyword identifies that you want to limit EditHighScoreViewControllerDelegate to class types.
If you’re asking why that is, it’s because you mark any references to this protocol as weak. To have weak references, you need a protocol that can only be used with a reference type.
You’ll read about weak references a bit further along in this chapter; this might all become a little bit clearer at that point.
Protocols [TODO: delete me?]
In Swift, a protocol has nothing to do with computer networks or meeting royalty, it’s simply a name for a group of methods.
A protocol doesn’t usually 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 you don’t need to get into right now.
The two methods listed in EditHighScoreViewControllerDelegate are:
editHighScoreViewControllerDidCancel(_:)editHighScoreViewController(_:didFinishAdding:)
Delegates often have very long method names!
The first method is for when the user presses Cancel, while the second is for when they press Done. In the latter case, didFinishAdding passes along the updated HighScoreItem.
For HighScoresViewController to conform to this protocol, it must provide implementations for these two methods. From then on, you can refer to HighScoresViewController using the protocol name instead of the class name.
If you’ve programmed in other languages, you may recognize protocols as being very similar to interfaces.
In EditHighScoreViewController, you can use the following to refer back to HighScoresViewController:
var delegate: EditHighScoreViewControllerDelegate
The variable delegate is nothing more than a reference to some object that implements the methods of EditHighScoreViewControllerDelegate. You can send messages to the object that the delegate variable references without knowing what kind of object it really is.
Of course, you know the object referenced by delegate is the HighScoresViewController, but EditHighScoreViewController doesn’t need to be aware of that. All it sees is some object that implements its delegate protocol.
If you wanted, you could make some other object implement the protocol; EditHighScoreViewController would be perfectly fine with that. That’s the power of delegation: You have removed – or abstracted away – the dependency between the EditHighScoreViewController 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!
Notifying the delegate
You’re not done in EditHighScoreViewController.swift yet. The view controller needs a property that it can use to refer to the delegate; you’ll take care of that now.
➤ Add this inside the EditHighScoreViewController class, below the outlets:
weak var delegate: EditHighScoreViewControllerDelegate?
It looks like a regular instance variable declaration, with two differences: weak and the question mark.
Delegates are usually declared as being weak. This is not a statement of their moral character, but rather a way to describe the relationship between the view controller and its delegate. Delegates are also optional, as indicated by the question mark, which you learned a bit about in the previous chapter.
You’ll learn more about what that means in a moment.
➤ Add this below the delegate declaration:
var highScoreItem: HighScoreItem!
You’ll use this to store the item you’re editing.
➤ Replace the cancel() and done() actions with the following:
@IBAction func cancel() {
delegate?.editHighScoreViewControllerDidCancel(self)
}
@IBAction func done() {
highScoreItem.name = textField.text!
delegate?.editHighScoreViewController(self, didFinishEditing: highScoreItem)
}
Now, look at the changes you made. When the user taps the Cancel button, you send the editHighScoreViewControllerDidCancel(_:) message back to the delegate.
You do something similar for the Done button, except that the message is editHighScoreViewController(_:didFinishEditing:) and you pass along HighScoreItem, which has the text string from the text field.
Note: It’s customary for the delegate methods to have a reference to their owner as the first (or only) parameter.
Doing this is not required, but it’s a good idea. For example, in the case of Table views, an object may be a delegate or data source for more than one Table view. In that case, you’ll need to be able to distinguish between those Table views. To allow for this, the Table view delegate methods have a parameter for the
UITableViewthat 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:EditHighScoreViewController, in this case. It’s also why all the delegate method names start witheditHighScoreViewController.
➤ Run the app and try the Cancel and Done buttons. They no longer work!
Hopefully, you’re not too surprised! The Edit High Score screen now depends on a delegate to make it close, but you haven’t told it 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
You read a few times before 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. Swift doesn’t allow this for normal variables.
The problem with nil and NULL is that they frequently cause apps to crash. If an app attempts to use a variable that is nil when you expect it to have a value, the app will crash. This is the dreaded null pointer dereference error.
Swift avoids these crashes 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 optional can have a nil value.
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’s a question mark behind the type:
weak var delegate: EditHighScoreViewControllerDelegate?
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? Well, there are two reasons.
Often, delegates are truly optional; a UITableView works fine even if you don’t implement any of its delegate methods, although you do need to provide at least some of its data source methods.
More importantly, when you load EditHighScoreViewController from the storyboard and instantiate it, 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 only temporarily, 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?.editHighScoreViewControllerDidCancel(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, delegate should never be nil – that would get users stuck on the Edit High Score screen. However, 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 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.
Conforming to the delegate protocol
Before you can give EditHighScoreViewController its delegate, you need to make HighScoresViewController suitable to play the role of a delegate.
➤ In HighScoresViewController.swift, change the class line to the following (this all goes on one line):
class HighScoresViewController: UITableViewController,
EditHighScoreViewControllerDelegate {
This tells the compiler that HighScoresViewController now promises to follow the EditHighScoreViewControllerDelegate protocol. Or, in programming terminology, that it conforms to the EditHighScoreViewControllerDelegate protocol.
Xcode should now throw up an error: “Type HighScoresViewController does not conform to protocol EditHighScoreViewControllerDelegate.”
That is correct: You still need to add the methods that are listed in EditHighScoreViewControllerDelegate. With the latest version of Xcode, there’s an easy way to get started fixing this issue — see that Fix button? Simply click it.
Xcode will add in the stubs, or the bare minimum code, for the missing methods. You’ll have to add in the actual implementation for each method, of course.
➤ Add the implementations for the protocol methods to HighScoresViewController:
// MARK:- Edit High Score ViewController Delegates
func editHighScoreViewControllerDidCancel(
_ controller: EditHighScoreViewController) {
navigationController?.popViewController(animated:true)
}
func editHighScoreViewController(
_ controller: EditHighScoreViewController,
didFinishEditing item: HighScoreItem) {
navigationController?.popViewController(animated:true)
}
Currently, both methods simply close the Edit High Score screen, which is what the EditHighScoreViewController used to do with its cancel() and done() actions. You’ve simply moved that responsibility to the delegate. You haven’t added the code that updates the HighScoreItem object in the Table view yet. 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 the first four steps, so there’s just one more thing you need to do — step 5: tell EditHighScoreViewController that HighScoresViewController is its delegate.
The proper place to do that is in the prepare(for:sender:) method, also known as prepare-for-segue.
UIKit invokes prepare(for:sender:) right before it performs a segue from one screen to another. Remember 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 displaying it. You’ll usually do this by setting one or more of the new view controller’s properties.
➤ Add this method to HighScoresViewController.swift:
// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
// 1
let controller = segue.destination as! EditHighScoreViewController
// 2
controller.delegate = self
// 3
if let indexPath = tableView.indexPath(for: sender as! UITableViewCell) {
controller.highScoreItem = items[indexPath.row]
}
}
This is what the above code does, step by step:
-
You find the new view controller that you want to display in
segue.destination.destinationis of typeUIViewContoller, since the new view controller could be any view controller subclass.To handle that issue, you cast
destinationtoEditHighScoreViewControllerto 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 a
nilvalue. Casting works here becauseEditHighScoreViewControlleris a sub-class ofUIViewContoller. [TODO: FPE: Is this spacing OK? I can’t see how it renders, so I can’t verify.] -
Once you have a reference to
EditHighScoreViewController, you set itsdelegateproperty toself. This tellsEditHighScoreViewControllerthat from now on, the object identified asselfis its delegate. But what is “self” here? Well, since you’re editing HighScoresViewController.swift,selfrefers toHighScoresViewController. -
The parameter
sendercontains a reference to the control that triggered the segue. In this case, it refers to the Table view cell which you tapped.You use that
UITableViewCellobject to find the Table view row number by looking up the corresponding index path usingtableView.indexPath(for:).The return type of
indexPath(for:)isIndexPath?, an optional, meaning it could returnnil. That’s why you need to unwrap this optional value withif letbefore you can use it.Once you have the index path, you obtain the
HighScoreItemobject to edit and you assign it toEditHighScoreViewController’shighScoreItemproperty.
Excellent! HighScoresViewController is now the delegate of EditHighScoreViewController. It took some work, but you’re almost set now.
Now that you have a reference to the item you’re editing, you can make the screen actually show the name of the current high scorer before you edit it.
➤ Open EditHighScoreViewController.swift and add this to viewDidLoad():
textField.text = highScoreItem.name
➤ Run the app, click on any high score and see that the edit item screen now has the current name already in the text field.
Updating the table view
If you change the name and click Done, you’ll update the item itself, but the Table view will still show the old value. That’s because you didn’t tell the Table view to refresh the cell after the data changed. Time to fix it!
➤ Change the implementation of the didFinishEditing delegate method in HighScoresViewController.swift to the following:
func editHighScoreViewController(_ controller: EditHighScoreViewController,
didFinishEditing item: HighScoreItem) {
// 1
if let index = items.firstIndex(of: item) {
// 2
let indexPath = IndexPath(row: index, section: 0)
let indexPaths = [indexPath]
// 3
tableView.reloadRows(at: indexPaths, with: .automatic)
}
// 4
PersistencyHelper.saveHighScores(items)
navigationController?.popViewController(animated:true)
}
Here’s what this new code does:
-
To update the cell, you need the
IndexPathof that cell. The cell index is the same as the index ofHighScoreItemin theitemsarray. You can usefirstIndex(of:)to return that index.Now, it won’t happen here, but it’s possible that you could use
index(of:)on an object that’s not actually in the array. To account for that possibility,index(of:)doesn’t return a normal value, it returns an optional. If the object is not part of the array, the returned value isnil.That’s why you need to use
if lethere to unwrap the return value fromindex(of:). -
You create a new array of
IndexPathobjects to update. In this case, there’s only one cell you want to update so you create an array with a single index path. -
You tell the Table view to update that specific cell. Keep in mind that, should you ever need to, you can also use
insertRows(at:with:)anddeleteRows(at:with:). -
Eventually, you save the updated list of high scores and pop [TODO: FPE: Is “pop” right word? Should it be “populate” instead?] the view controller
➤ Try to build the app. Oops, Xcode has found another reason to complain:
Xcode displays this error because you can’t use firstIndex(of:) on just any array, or collection of objects. An object has to be “equatable” if you are to use firstIndex(of:) on an array of that object type.
That’s because firstIndex(of:) needs a way to compare the object that you’re looking for against the objects in the array, to see if they are equal.
Your HighScoreItem object does not have any functionality for that yet. There are a few ways you can fix this, but in this case, you¹ll use the easy one.
➤ In HighScoreItem.swift, change the class line to:
class HighScoreItem : NSObject, Codable {
If you’ve programmed in Objective-C before, you’ll be familiar with NSObject.
Almost all objects in Objective-C programs are based on NSObject. It’s the most basic building block that iOS provides, and it offers a bunch of useful functionality that standard Swift objects don’t have.
You can write many Swift programs without having to resort to NSObject but in times like these, it comes in handy.
Building HighScoreItem on top of NSObject is enough to satisfy the “equatable” requirement. In case you’re interested, the other way to do this would have been to specify that HighScoreItem conforms to the Equatable protocol. But then you’d have to implement an additional method to indicate how the comparison of two HighScoreItem instances would happen. So going with NSObject conformance is easier, for the time being.
➤ Build and run the app again and verify that editing items works now. Excellent!
Weak
I promised you an explanation of the weak keyword. Relationships between objects can be weak or strong. You use weak relationships to avoid 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, you destroy, or deallocate, an object 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 isn’t destroyed, even though it should be, 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, you should always make delegates weak.
There is another relationship type that is similar to weak and that you can also use delegates: unowned. The difference is that weak variables can be nil. However, you don’t need to worry about that right now.
Usually, you also declare @IBOutlets 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 24-Delegates and Protocols in the Source Code folder.