[Updated 2/13/2018 to Xcode 9 / Swift 4.0]
It should be one of the easiest things we do, and yet for many it is the most confusing. Getting data from one view controller to another as you switch views never seems easy. Segues might be slightly difficult, but delegates to get the information back have given many a developer a headache or two. In this lesson, we’ll discuss this often used technique. This lesson will make a new small app which is nothing but a segue, a delegate, and a few controls to prove they work.
Make a New App
The First Scene
Make a new single-view Swift project named DelegateExample
. In the storyboard click on the view controller, and embed it in a navigation controller by selecting Editor>Embed In> Navigation Controller.
Press Command-N to make a new file. Select a Cocoa Touch Class for the file and name the file FooOneViewController with UIViewController
as a subclass:
Go back to the storyboard, select the view controller and change the class to FooOneViewController in the identity inspector
Add a label in the center of the view, centered and with the text Unknown Color
. Change the font to System Black 26point. Drag a bar button item to the right side of the navigation bar. Change its title to Color
. It should look something like this.
Open the assistant editor. In the view controller, remove everything but the viewDidLoad()
method. Control-drag connect an outlet for the Unknown Color label called colorLabel
. Your code should look like this:
class FooOneViewController: UIViewController { @IBOutlet weak var colorLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() } }
The Second Scene
Like our first controller, Press Command-N and make a new Cocoa Touch class that subclasses UIViewController
. Call it FooTwoViewController
. In the code, remove everything but the viewDidLoad()
method.
Go back to the storyboard and drag in another view controller. From the Foo One Navigation bar, control-drag the Color Bar button item to the new controller to connect the segue. Set the segue to show.
In the attributes inspector set the identifier to mySegue
.
Select the new view controller again in the storyboard. Once we’ve set the segue, in the identity inspector set the custom class for this view controller to be FooTwoViewController
like we did the first class
Now we’ll add a label, a navigation item, a bar button item, and three buttons. First add the label and place it towards the center of the view. Make the label the width of the view. Place the three buttons under the label. Title the buttons Red
, Green
and Blue
. Make their background colors red, green and blue. Drag a navigation item into the view and in the label, title it Foo Two
. Finally add the bar button item to the navigation bar. Your finished view should look like this:
Open the assistant editor if not already open. Control-drag the label into the FooTwoViewController
class. Create an outlet named colorLabel
. Select the bar button, and control-drag it to the class code. Make an action named saveColor
.
Select the Red button, and control drag it into FooTwoViewController
. Make an action for a UIButton
called colorSelectionButton
.
Place your mouse cursor over the circle in front of the color selection button. A plus should appear Drag from that plus to the green button to connect it. Do the same for the blue button.
Close the assistant editor. Go to FooTwoViewController Above the colorLabel
outlet add a string variable colorString
:
var colorString = "I don't know the color"
In the colorSelectionButton
method add this code:
@IBAction func colorSelectionButton(_ sender: UIButton) { colorString = (sender.titleLabel?.text)! colorLabel.text = colorString }
Finally, add this to viewDidLoad()
to set the label correctly from the property:
colorLabel.text = colorString
Your code for FooTwoViewController
should look like this:
import UIKit class FooTwoViewController: UIViewController { var colorString = "I don't know the color" @IBOutlet weak var colorLabel: UILabel! @IBAction func saveColor(_ sender: Any) { } @IBAction func colorSelectionButton(_ sender: UIButton) { colorString = (sender.titleLabel?.text)! colorLabel.text = colorString } override func viewDidLoad() { super.viewDidLoad() colorLabel.text = colorString } }
Adding prepare( for segue:)
To move data from Foo One to Foo Two, we will override prepare(for segue:)
. If you have the assistant editor open, close it. Go into the FooOneViewController.Swift
file and under the viewDidLoad
method but before the end of the class, type prepare
. Xcode will show you choices in the drop down menu
Click the selection for prepare( for segue:
on the drop down. You’ll have this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { code }
Add the following code to this method so the prepare(for Segue: )
reads:
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "mySegue"{ let vc = segue.destination as! FooTwoViewController vc.colorString = colorLabel.text! } }
In line 1, the first parameter is segue
, which is a UIStoryboardSegue
object. This is the segue we call for our transition between controllers. In line 2 we check to see if this is the correct segue. This is line of code where a lot of bugs happen. Be sure that the string you are comparing matches exactly the segue identifier set in the storyboard, otherwise the code won’t do anything. Line 3 we create vc
which is the pointer to the segue’s destination view controller. destination
is of type UIViewController
. Swift will only assume vc is a type UIViewController, not our specific instance of it. We need to downcast to the correct object type with the as!
operator, and make vc
an instance of FooTwoController
. Once we do that, we can access the colorString
property in line 4, sending our current text to the property in the new controller.
Build and Run. We now can send “Unknown Color” to the new controller.
Now comes the slightly more challenging part: getting stuff back. For that we will need a delegate.
Protocols and Delegates in Swift
Delegation uses protocols. Protocols are a set of properties and methods that while declared in one place, another class implements. They allow for a layer of abstraction. A class adopts a protocol to do something. The protocol defines what the something is. The adopting class will have the code how it gets done. Using delegates and protocols is one of the hardest concepts for most developers to understand. For details about how this all works, I suggest reading the lesson Why Do We Need Delegates?
We are going to set up a protocol to dismiss the Foo Two controller, and then adopt it in the main view controller. This way we can indirectly access properties of the Foo Two controller. Such an access is a delegate. In the Foo Two controller above the class definition for FooTwoViewController
, add the following:
protocol FooTwoViewControllerDelegate{ func didSelectColor(controller:FooTwoViewController,text:String) }
This sets up the protocol FooTwoViewControllerDelegate
with a required method didSelectColor
. Next, in the FooTwoViewController
class, add the following just after the class definition:
var delegate:FooTwoViewControllerDelegate! = nil
We define a delegate of optional type FooTwoViewControllerDelegate
and set its value to nil
. We are familiar with simple values such as a Double
or Int
. If we want a state for any object that has a nil
state, we use an optional value. Declare an optional with a question mark after the type, as it is above. An optional value of a Double?
can be nil
, along with any number for example. There is a cost to this extra power. To get back that number you have to unwrap the optional value. You can use the !
or ?
operator to return a value as long as the optional is non-nil. If nil
, it will break program execution with a run-time error.
unexpectedly found nil while unwrapping an Optional value
Before unwrapping an optional value, it’s usually a good idea to check for nil
. There are several ways of doing that. For example, we could write the saveColor
method like this:
@IBAction func saveColor(sender : UIBarButtonItem) { if (delegate != nil) { delegate!.didSelectColor(self, text: colorLabel!.text!) } }
This will prevent run-time errors. However there are situations where run time errors are telling you exactly what’s wrong in your application. For delegates, I often don’t check delegate
for nil
. If I get the nil
error message, I know I didn’t set the delegate right.
@IBAction func saveColor(sender : UIBarButtonItem) { delegate?.didSelectColor(self, text: colorLabel!.text!) }
There is one other way to handle this situation, and that is to use the guard
keyword to unwrap your optional. Add this one to our code.
@IBAction func saveColor(_ sender: UIBarButtonItem) { guard let delegate = self.delegate else { print("Delegate for FooTwoDelegateController not Set") return } delegate.didSelectColor(controller: self, text: colorLabel.text!) }
Guard creates a new local constant called delegate
that isn’t optional. If nil
it runs code to print to the console we didn’t set the delegate. If not we run the didSelectColor
method, sending to this function colorLabel.text!
. As a protocol, it isn’t defined here. The adopting class defines it, so our next stop is adopting the protocol back at the original view controller.
Open the FooOneViewController.swift file. Change the class definition to:
class FooOneViewController: UIViewController,FooTwoViewControllerDelegate
You should immediately get this error:
We need to write the protocol. An easy way to have all the required protocols added for you is to click the Fix button.
Click the button, and you get this under the class declaration.
func didSelectColor(controller: FooTwoViewController, text: String) { code }
I don’t like my delegate methods on the top of my class, but the bottom. Select and cut this stub. Below the prepare(for segue:_
, add a MARK: comment for delegates, and paste the code in
//MARK: - Delegates func didSelectColor(controller: FooTwoViewController, text: String) { code }
I’ll do three things in this delegate. first I’ll change colorLabel
‘s text:
func didSelectColor(controller: FooTwoViewController, text: String) { colorLabel.text = "The Color is " + text
This is the sneaky part. The arguments go the function are values from FooTwoViewController
, but the function is in FooOneViewController
. We take the value of text
and place it in our label. I can use this to change the color for the label text too
case "Red": colorLabel.textColor = .red case "Blue": colorLabel.textColor = .blue case "Green": colorLabel.textColor = .green default: colorLabel.textColor = .black }
The third thing we can do is use the controller
parameter to dismiss the controller with popViewControllerAnimated
.
controller.navigationController?.popViewController(animated: true)
I use this sometimes if I know I’m dismissing immediately from the destination controller. If I’m not, and have some housekeeping to do, I’ll just transfer the data in the delegate and dismiss from the destination controller, in this case fooTwo.
Build and Run.
Tap Color, then Green then Save. On the Console you’ll get an error message:
Delegate for FooTwoDelegateController not Set
This is the most common error with delegates. It is also why I prefer a fatal error to a soft notification on the console. We need to set up the delegate in the segue. Change the prepare(for segue:)
, adding the highlighted line
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "mySegue"{ let vc = segue.destination as! FooTwoViewController vc.colorString = colorLabel.text! vc.delegate = self } }
Build and run. You should be able to send “unknown” to Foo Two. Click a color and send the color back.
The Steps to a Delegate
Here’s a summary of this steps to setting up delegation in Navigation controllers
- Between two view controllers, set up a segue with a segue identifier
- In the source view controller, set up the
prepare( for segue:)
to send the destination any values necessary. Test this works. - Add a protocol to the destination view controller with a method declaration.
- Make the protocol and optional value named delegate in the destination controller.
- When you are ready to dismiss the controller and send back values, call the
delegate
method. - Adopt the protocol in the source view controller
- Add the required delegate method to the source view controller
- Add to
prepare( for segue:)
the statementvc.delegate=self
If you are not understanding what we did here, you might need a little help with a few background concepts of delegation, MVC and encapsulation. This is the how, not why. For the why, read Why Do We Need Delegates
The Whole Code
Below you’ll find the complete listing. You’ll find the completed code over on gitHub here:
FooOneViewController.swift
// // FooOneViewController.swift // DelegateExample // // Created by Steven Lipton on 2/13/18. // Copyright © 2018 Steven Lipton. All rights reserved. // import UIKit class FooOneViewController: UIViewController,FooTwoViewControllerDelegate { @IBOutlet weak var colorLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "mySegue"{ let vc = segue.destination as! FooTwoViewController vc.colorString = colorLabel.text! vc.delegate = self } } //MARK: - Delegates func didSelectColor(controller: FooTwoViewController, text: String) { colorLabel.text = "The Color is " + text switch text{ case "Red": colorLabel.textColor = .red case "Blue": colorLabel.textColor = .blue case "Green": colorLabel.textColor = .green default: colorLabel.textColor = .black } controller.navigationController?.popViewController(animated: true) } }
FooTwoViewController.swift
// // FooTwoViewController.swift // DelegateExample // // Created by Steven Lipton on 2/13/18. // Copyright © 2018 Steven Lipton. All rights reserved. // import UIKit protocol FooTwoViewControllerDelegate{ func didSelectColor(controller:FooTwoViewController,text:String) } class FooTwoViewController: UIViewController { var colorString = "I don't know the color" var delegate:FooTwoViewControllerDelegate! = nil @IBOutlet weak var colorLabel: UILabel! @IBAction func saveColor(_ sender: Any) { guard let delegate = self.delegate else { print("Delegate for FooTwoDelegateController not set") return } delegate.didSelectColor(controller: self, text: colorString) } @IBAction func colorSelectionButton(_ sender: UIButton) { colorString = (sender.titleLabel?.text)! colorLabel.text = colorString } override func viewDidLoad() { super.viewDidLoad() colorLabel.text = colorString } }
Leave a Reply