Passing Data Between UIViewControllers

Passing Data Between UIViewControllers

Overview of using Class Variables, Segues, Delegates, Closures, and Broadcasting when communicating information between UIViewControllers.

Go to the profile of  Jonathan Banks
Jonathan Banks
15 min read
Time
45mins
Platforms
iOS
Difficulty
Easy
Technologies
Swift

Review the many different ways developers can pass data between UIViewControllers with detailed code examples and breakdowns.

Overview

Passing data between UIViewControllers requires a good understanding of the suite of options provided by UIKit. The goal of this article is to summarize and simplify that information into a short and useful guide.

The various options broken into categories are as follows:

  • Passing Data Forward: the UIViewController creates an instance of a new UIViewController to push onto the stack. Prior to pushing on the stack, it sets the value of that new instance's member variable(s).
  • Passing Data Back: UIViewController will be removed from the stack and passes data back to the previous UIViewController prior to removal.
  • Broadcasting: publishers pass data either forward or back to a single or multiple observers.

Moving on, a detailed runthrough of each listed option with a focus on concrete code examples is next on the agenda.

Passing Data Forward

Use this method when creating a UIViewController in code or accessing one created in Storyboard via segue, Typically, the newly created UIViewController will change values, appearance, user options, or more, based on user input or selection from the previous UIViewController.

Class Variable Method

Put publicly accessible variables into the custom subclass of UIViewController and set the value on creation.


//FILE: BaseViewController
class BaseViewController: UIViewController{
    ...
    
    // Action wired to a button on the MainViewController
    @IBAction triggerSomething(){
        let passDataVC = PassDataToMeViewController()
        
        // Set the data 
        passDataVC.data = 1234
        self.present(passDataVC, animated:true, completion:nil)
    }

}

/////////////////////////////////////////////////////////////////////

//FILE: PassDataToMeViewController
class PassDataToMeViewController: UIViewController{

    public var data:Int = 0
    
    ....
}

Breaking down that code:

  • BaseViewController creates an instance of the PassDataToMeViewController.
  • PassDataToMeViewController exposes its data class variable publicly
  • BaseViewController sets data with a value and then calls the present(_:animated:completion:) method to push the UIViewController onto the stack.

Segue Method

Take advantage of Storyboard segues to populate class variables when they're created but before being pushed on the stack. The technique is done by overriding the prepare() function and setting the data before the transition.

In this example, PassDataToMeViewController is not created in code, instead its creation is invoked when the performSegue() function is called in code.

Make sure to assign an Identifier to the segue created in Storyboard by selecting the segue, accessing the Attributes Inspector, and giving it an Identifier name.

Screen-Shot-2019-07-29-at-11.06.15-AM

Here's the code to use the segue now assigned an Identifier:

//FILE: BaseViewController
class BaseViewController: UIViewController{
    ...
    
    // Action wired to a button on the MainViewController
    @IBAction triggerSomething(){
        performSegue(withIdentifier:"segueToPassDataToMeViewController", sender: self)
        
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?){
        
        if let pdvc = segue.destination as? PassDataToMeViewController{
            pdvc.data = 1234
        }
    }

/////////////////////////////////////////////////////////////////////


//FILE: PassDataForwardToMeViewController
class PassDataForwardToMeViewController: UIViewController{

    public var data:Int = 0
    
    ....
}

Breaking down the code into steps:

  1. User taps button that triggers a new screen, calling the triggerSomething() method.
  2. performSegue() is called prior to pushing the new UIViewController onto the stack. The UIKit deserializes the PassDataForwardToMeViewController and creates it.
  3. BaseViewController is still on top of the stack and visible to user.
  4. UIKit calls the prepare() function, just prior to pushing PassDataForwardToMeViewController onto the stack. prepare() permits the programmer to do last minute updates before it's visible to user.
  5. The data variable in PassDataForwardToMeViewController is updated with 1234
  6. PassDataForwardToMeViewController is pushed onto the stack and shown to the user.

There is a requirement for the current UIViewController to have been automatically loaded from the Storyboard. In other words, creation of an instance of UIViewController in code and then calling performSegue() is not permitted and will throw an exception.

Useful Links:

Passing Data Back

Use when returning to the previous UIViewController and passing data back to the creator UIViewController is required.

Delegate Method

A great way to think about Delegates is to consider them callback mechanisms. Refer to the diagram for a complimentary visual aid:

delegate

  • The Delegate implements the Protocol and becomes both its original type as well as the Protocol type.
  • The Delegator maintains an instance of the Delegate who has implemented the Protocol and is of Protocol type through that implementation.
  • The Delegator uses that instance of the Delegate to callback to the Delegate using its Protocol functions.

How does this apply to the built-in iOS controls that incorporate delegation?

Take a look at the diagram below showing a user-created UIViewController. It has a class variable of UITableView and has implemented the associated protocols UITableViewDelegate and UITableViewDataSource.

uitableview--3-

Breaking it down:

  • UIViewController implements the UITableViewDelegate and UITableViewDataSource protocol
  • UIViewController creates a class variable of type UITableView and sets its delegate to self. This means the UIViewController should implement the associated protocols.
  • When the UITableView code executes something where delegates must be updated, it will call the necessary method on its own delegate, in this case that's the UIViewController, which executes its own requisite implementation of the function specified in the protocol and implemented in the UIViewController.

Implementing in Code

With the background and diagrams complete, here's a concrete example, building on the code shown previously for BaseViewController and PassDataForwardToMeViewController.

//FILE: MyDelegate

protocol MyDelegate{
    func whizbang(data:Int)
}

///////////////////////////////////////////////////////////////////

//FILE: BaseViewController
class BaseViewController: UIViewController{
    ...
    var whizBang:Int = 0
    
     @IBAction triggerSomething(){
        let passDataVC = PassDataToMeViewController()
        passDataVC.data = 1234
        passDataVC.delegate = self
        self.present(passDataVC, animated:true, completion:nil)
    }
    
}

extension BaseViewController:MyDelegate{
    
    func whizbang(data:Int){
        self.whizBang = data
    }   
}

//////////////////////////////////////////////////////////////////////

//FILE: PassDataForwardToMeViewController
class PassDataForwardToMeViewController: UIViewController{

    public var data:Int = 0
    var delegate:MyDelegate
    
    func setData(){
        self.data = 5678
    }
    
    ...
    @IBAction triggerSomethingDataNeededInBaseVC(){
        self.delegate.whizbang(data:self.data)
    }
}

Breaking down this code:

  1. Define a protocol named MyDelegate
  2. Implement the protocol MyDelegate in BaseViewController by defining the function whizbang(data:)
  3. Create an instance of MyDelegate delegate in PassDataForwardToMeViewController
  4. The VC is presented to the user, interaction happens, and somewhere else in the code (not shown), the setData() function is called and the class variable data is set to 5678
  5. The user triggers a return to the BaseViewController through the triggerSomethingDataNeededInBaseVC() IBAction. This method uses is MyDelegate instance, delegate, to callback to BaseViewController using the Protocol function whizbang(data:).

In picture form to match the generic diagram of Delegates above:

delegate22

Delegates resolve many issues that would be very challenging without the design pattern. They are plentiful throughout iOS so its highly recommended to use them and get used to them.

Useful Links:

Unwind Segue Method

The next option takes advantage of the power in segues to pass data back to an originating "parent" UIViewController. These special segues are known as unwind segues.

Create an Unwind Segue Function in the Parent Class

The first step is to create an @IBAction method in the parent UIViewController. Using the existing code, the parent is the BaseViewController. Refer to the sample code showing the creation of that method: unwindFromPassDataToMeViewController():

//FILE: BaseViewController
class BaseViewController: UIViewController{
    ...
    var whizBang:Int = 0
    
     @IBAction triggerSomething(){
        let passDataVC = PassDataToMeViewController()
        passDataVC.data = 1234
        passDataVC.delegate = self
        self.present(passDataVC, animated:true, completion:nil)
    }
    

     @IBAction func unwindFromPassDataToMeViewController(_ sender:UIStoryboardSegue){
         if let pdtmVC = sender.source as? PassDataToMeViewController{
             self.wizBang = pdtmVC.data 
     }
 }
 
 ...

The unwindFromPassDataToMeViewController(_ sender:UIStoryboardSegue) unwind segue function will grab the data from the PassDataForwardToMeViewController directly because it can :

  • Access the parameter sender of type UIStoryboardSegue
  • Tap its source attribute and cast the Optional to the PassDataForwardToMeViewController class, because the unwinding originates from that class.
  • Subsequently access any public class variables from PassDataForwardToMeViewController and set local data based on the the values.

Finalize in Interface Builder

Control-click the ViewController icon above the child UIViewController, drag over to the exit icon on the right, and select the function name that was just created, unwindFromPassDataToMeViewController(_ sender:UIStoryboardSegue).

The action is shown below but from a different project so the function name is not the same but the physical operation is the same:

ezgif-5-bd95ccf420ca

Once a UIViewController has a method with this signature it can identify the forward segue to the created UIViewController:

@IBAction func <MY_CUSTOM_FUNCTION_NAME>(_ sender:UIStoryboardSegue){

Interface Builder will present any function in the creator that has this unwind "signature" to select.

It seems like magic but it's not. The fantastic team of coders that create and update XCode and Interface Builder do seem to do magical work though.

Connect the Unwind Trigger Button/IBAction in Child to Exit

Lastly, add a trigger in the created UIViewController that will initiate the return to the creator UIViewController

In Interface Builder, control-drag from the button or other @IBAction to the same Exit icon at the top of the child UIViewController. Select the same unwind function unwindFromPassDataToMeViewController() as before.

That's all there is to it! Now with this and the previous information provided here, data may be sent forward to a created UIViewController as well as back to the creator using only segues and a small amount of code.

Closures Method

According to Apple's documentation:

Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.

Closures can capture and store references to any constants and variables from the context in which they are defined. This is known as closing over those constants and variables. Swift handles all of the memory management of capturing for you.

In layman's terms, this means code gets passed between different parts of the application and the data encapsulated from the external environment. Manipulate it immediately or at a later time.

Returning to passing data between ViewControllers, a sending UIViewController encapsulates an operation or a values inside a closure and passes it to a receiving UIViewController. The closures are passed as arguments to methods and assigned to variables used at a later time.

All of this sounds promising! The next section will detail closures. If familiar with them already, skip straight to incorporating them into intra-UIViewController communication here.

Brief Overview of Closures

The basic syntax of a simple closure is:

{ (parameters) -> return type in
    statements
}

The in keyword tells the compiler that the closure declaration is complete and the body of the closure commences after.

Turning that syntax into a concrete example of a closure with code:

//Environment Variable
var modByClosure = 5

// Declare closure
var theClosure = {
    modByClosure += 5
}

//Call closure
theClosure()

Breaking this down:

  • The modByClosure variable is in-scope to theClosure.
  • The closure assigned to a variable named theClosure has a signature or closure type of () -> (). This indicates there are no parameters incoming to the closure and a return value of Void.
  • When the closure assigned to the variable theClosure is executed, 5 is added to the environment variable modByClosure that has captured the value of 5.
  • The closure can be executed at any time and will have access to the current value in modByClosure.
  • If modByClosure is modified in-between capturing it in the closure and when that closure is executed the most current value is used by the closure.

Closure Types

The closure type is defined by how it is declared. In other words, the parameters and return type taken together encompass the closure type. Using the code above as an example, the type of closure named theClosure is () -> () This is considered a Void->Void closure type because it has no parameters and no return value.

What about more closures that have multiple parameters and more complex return types? Simply substitute in the parameters and return types. If the closure above should take in an Int as parameter then the closure type becomes (Int) -> (). It can also be written (Int) -> Void. Here's a few more examples that are a bit more complex:

  • (Int, Double) -> (Int): takes in an Int and a Double then returns an Int
  • ([Int], [String:String]) -> ([Int:Int]): takes in an Int array, a Dictionary of String->String mapping and returns a Dictionary of Int->Int mapping.

Declaring Closures

Simple closures of type ()->() are covered. Now it's time to dig in a little more and see how to declare more complex closures. These are the kind that used to pass simple data around UIViewControllers

The comments in each example below explain the functionality of each closure:

Closure with Parameter

// Declare closure with Int parameter
var theClosure:(Int) -> () = { addValue in
    modByClosure += addValue
}

// Call it
theClosure(1234)

Closure Returning a Value

//Environment variable
var modByClosure = 5

//Declare closure with Int parameter and String return
var theClosure:(Int) -> (String) = { addValue in
    modByClosure += addValue
    return "Added"
}

// Call it
let completionStr = theClosure(1234)

Closure as a Parameter of a Function


//Declare the Closure
var theClosure:(Int) -> (String) = {addValue in
    modByClosure += addValue
    return "modByClosure = \(modByClosure)"
}

// Declare the Function that accepts the closure parameter
func addingFunctionWithClosureParameter(addingClosure:(Int)->(String)){
    let retStr = addingClosure(1234)
    print(retStr)
}

//Call the function with declared closure as parameter
addingFunctionWithClosureParameter(addingClosure:theClosure )

//Call the function with anonymous closure as parameter
addingFunction(addingClosure: { (addValue:Int) -> (String) in
    modByClosure += addValue
    return "modByClosure = \(modByClosure)"  
})

Escaping vs Non-Escaping Closures

Closures passed as parameters into functions are either @escaping or non-escaping. All examples provided thus far are non-escaping closures. The default setting is non-escaping unless the escaping keyword is used. The difference between escaping and non-escaping:

  • Non-Escaping Closures
    • Closure executed before the function returns.
    • When the function has returned the closure is no longer in memory.
  • Escaping Closures
    • Closure executed at some time after the method returns
    • Closure remains in memory when the method returns.

When should a closure exist past the lifetime of the function? Generally, those cases involve a function that performs asynchronous tasks on a background thread. Such functions return before the task is complete so post-processing happens in the @escaping closure. Examples might be:

  • Sorting a very large array and then transforming each element once it's complete.
  • Making a network API call, waiting for the response, then processing the JSON or XML.

Make a closure escaping by adding the keyword before the declaration of the closure type. Remember, closures are automatically non-escaping when the explicit escaping keyword is not added.

The following code takes the previous example of a closure as a parameter of a function and demonstrates it as an escaping closure:

//Declare the Closure
var theClosure:(Int) -> (String) = {addValue in
    modByClosure += addValue
    return "modByClosure = \(modByClosure)"
}

// Declare the Function that accepts the closure parameter
func addingFunctionWithClosureParameter(addingClosure: @escaping (Int)->(String)){
     
     //Trigger the call to the closure parameter after the function returns
     DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(5000)) {
        let retStr = addingClosure(1234)
        print(retStr)
    }
    
    return
}

//Call the function with declared closure as parameter
addingFunctionWithClosureParameter(addingClosure:theClosure )

//Call the function with anonymous closure as parameter
addingFunction(addingClosure: { (addValue:Int) -> (String) in
    modByClosure += addValue
    return "modByClosure = \(modByClosure)"  
})

The main changes are:

  • the addition of the @escaping keyword to the addingFunctionWithClosureParameter(addingClosure:) method
  • placing the call to the closure inside an asynchronous call.DispatchQueue adds a delay of 5 seconds.

If the async call using DispatchQueue is kept but the @escaping keyword is removed, the compiler will throw an error.

Applying Closures to UIViewController Communication

Given the detailed closure background, the next task is to modify the previous code to use them. First, a "Passing Data Back" example by replacing the Delegate used before with a closure solution.

The BaseViewController will receive data back from the PassDataToMeViewController when it triggers the BaseViewController closure.


//FILE: BaseViewController
class BaseViewController: UIViewController{
    ...
    var whizBang:Int = 0
  
    
    @IBAction triggerSomething(){
        let passDataVC = PassDataForwardToMeViewController()
        passDataVC.data = 1234
        
        // Set the closure to modify whizBang
        passDataVC.passBackClosure = {data in
            whizBang = data
        }
        
        self.present(passDataVC, animated:true, completion:nil)
    }
    
}

//////////////////////////////////////////////////////////////////////

//FILE: PassDataForwardToMeViewController
class PassDataForwardToMeViewController: UIViewController{

    public var data:Int = 0
    var delegate:MyDelegate
    var passBackClosure: ((Int)->())?
    
    func setData(){
        self.data = 5678
    }
    
    ...
    @IBAction triggerSomethingDataNeededInBaseVC(){
    
        // make sure the optional has been set before calling it
        if let pass_back_closure = passBackClosure{
            pass_back_closure(data)
        }
    }
}

The breakdown of the code is as follows:

  • Set the closure variable passBackClosure type in PassDataForwardToMeViewController as (Int)->() closure type. The closure will take an Int as a parameter and have no return value
  • In the triggerSomethingDataNeededInBaseVC() method, cast the optional closure to make sure it's been set in BaseViewController and call it with the current value in the local variable data.
  • When creating the instance of PassDataForwardToMeViewController by the BaseViewController, assign the closure that will be called from PassDataForwardToMeViewController.
    • This particular concrete closure will be called from PassDataForwardToMeViewController and will be used to set the local whizBang variable in BaseViewController.

Final Thoughts on Closures

Closures can get much more complex and it's recommended to investigate them in greater detail. Refer to the some quality links below:

Broadcast Method

At an extremely high level, NotificationCenter is based on the Observer design pattern which is a one-to-many "publish/subscribe" model. Ttake a look at what Wikipedia has to say about that design pattern, here:

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. The sole responsibility of a subject is to maintain a list of observers and to notify them of state changes by calling their update() operation.

The responsibility of observers is to register (and unregister) themselves on a subject (to get notified of state changes) and to update their state (synchronize their state with subject's state) when they are notified.

This makes subject and observers loosely coupled. Subject and observers have no explicit knowledge of each other. Observers can be added and removed independently at run-time.

These details sound an awful lot like NotificationCenter operation. Checking in more detail, examine the UML diagram of the pattern:

2880px-Observer_w_update.svg

Does that fit with Apple's documentation on NotificationCenter? Yes it does:

Objects register with a notification center to receive notifications (NSNotification objects) using the addObserver(_:selector:name:object:) or addObserver(forName:object:queue:using:) methods. When an object adds itself as an observer, it specifies which notifications it should receive. An object may therefore call this method several times in order to register itself as an observer for several different notifications.

Now that the architecture is understood, its time to use the previous code examples to build the NotificationCenter functionality.

Required Steps

The UIViewController receiving the data is the Observer that will register to receive the Notification and the UIViewController that's sending the data will broadcast the Notification.

These are the steps necessary to create and integrate Notifications into application architecture:

  • Create a custom Notification identifier
  • Write a function in the receiving UIViewController (Observer) class that will handle and process the custom Notification when it arrives.
  • Subscribe the UIViewController (Observer) to the custom Notification
  • Publish custom Notifications from the sending UIViewController (Broadcaster)

Broadcast Code with Notifications

The existing code needs modification to exclude delegate or closure code and add notifications and broadcast code. In this implementation the BaseViewController is the observer and BroadcastViewController is the publisher

First, create a custom Notification by extending Notification.Name and assign a static variable that can be freely accessed by any class.

//FILE: Extensions

// Create a custom Notification
extension Notification.Name {
    static let whizBangNotification = Notification.Name("whizbang")
}

The next steps are to:

  1. Create the function handleWhizBangNotification(notification:) that will be assigned as the Selector in the addObserver(:selector:name:object) method. It will be called when the observing class BaseViewController receives the custom Notification.
  2. Call the addObserver(:selector:name:object) method in viewDidLoad() to assign the BaseViewController as the Observer to posted custom Notification created above. Include the static name, .whizBangNotification, created in the previous step as the name parameter.
//////////////////////////////////////////////////////////////////////

//FILE: BaseViewController
class BaseViewController: UIViewController{
    ...
    var whizBang:Int = 0
    
    override func viewDidLoad(){
        ...
        
        // Add self as Observer and a function to handle it
        NotificationCenter.default.addObserver(self, 
                selector: #selector(handleWhizBangNotification(notification:)),
                name: .whizBangNotification, 
                object: nil)
    }
    
    // Function to handle incoming Notifications 
    @objc func handleWhizBangNotification(notification:Notification){
        if let t_whiz = notification.object as? Int{
            self.whizBang = t_whiz
        
        }
    }
    
    @IBAction triggerSomething(){
        let passDataVC = PassDataToMeViewController()
        self.present(passDataVC, animated:true, completion:nil)
    }
    
}

In the PassDataToMeViewController (Publisher), implement the post(name:object:) method, where the name is the static variable from the custom Notification and the object is the data to broadcast.


//FILE: PassDataToMeViewController
class PassDataToMeViewController: UIViewController{

    public var data:Int = 0
    var delegate:MyDelegate
    
    func setData(){
        self.data = 5678
    }
    
    ...
    @IBAction triggerSomethingDataNeededInBaseVC(){
    
        // Broadcast the Notification
       NotificationCenter.default.post(name: .whizBangNotification,
                                        object: self.data)
    }
}

Now, whenever the method named triggerSomethingDataNeededInBaseVC() is called, the Notification .whizBangNotification is broadcast to all observers registered to act upon it. The BaseViewController will pick it up and assign the updated data to its own whizBang variable.

Caveats

Using it this way is a bit of a subversion of the purpose of NotificationCenter. The intention of NotificationCenter is a one-to-many relationship between Observers and Broadcasters (or use the terminology Subscribers and Publishers) where multiple classes need to be updated with data.

In this scenario, there's a one-to-one relationship. Notifications add a lot of overhead to sending a message between two classes. Delegates, closures or unwind segues would make a lot more sense, at least for this contrived example.

On the other hand, if there are a larger number of UIViewControllers that require updating from a single source, this solution is slightly more appropriate. It has the added benefit of requiring the least amount of code as well.

Pick the solution that's easiest to understand. Hours of debugging a complex error just to use what someone on the internet thinks is more optimized is not wise. Those are hours best spent working on something else.

All This Information Is Great....Which One Do I Use Though?

There are so many ways to pass data between UIViewControllers it might result in decision paralysis. Ask these questions to help make a decision, based on a number of common scenarios:

  • Do you rely heavily on Interface Builder to create your apps? Segue/Unwind Segue Methods
  • Will you need to pass multiple types of complex data under different scenarios during the lifetime of the newly created UIViewController?
    • Do you want centralized control over the method definitions? Delegate Method
    • Would you rather pass data on an ad-hoc basis without requiring adherence to strict protocols? Closure Method
  • Do you have multiple UIViewControllers that must be updated on an ongoing basis? Broadcasting Method
  • Will you only need to pass simple data forward at creation time of the new UIViewController? Class Variable Method

Final Thoughts

The delegate solution should always be considered first. It's the most common pattern on the platform and helps decouple code into segregated classes. That will always be a huge benefit when debugging.

Closures are a steep challenge to those that have never seen them before. The syntax can also be difficult to remember on the fly. That said, they are the lightest weight and perhaps the easiest to maintain and troubleshoot.

Heavy users of Interface Builder should probably consider segues and unwind segues first but those that create everything in code would never give them a passing thought.

There is not a perfect solution for every application. The best suggestion if those new to the platform is to select the one that makes the most sense and master it before branching out to try the others.