Passing data between view controllers in iOS iOS 01.11.2019

Every controller takes care of a visual component on the screen. Some controllers are responsible for the whole screen estate; some take care of only a part of it.

The key concept in presenting new screens is a segue. This is a transition without interruption from one view controller to another. Using the segues, we can link together different scenes in our app and we can even transfer information between view controllers. Each segue can define different animations when transiting from once scene to another.

There are four main kinds of segues, each with its own unique approach and attributes, and which act differently depending on the size class they're in, or whether they're embedded in a navigation controller or a split view controller.

  • Show Detail. This segue is most useful for split view controllers. Split view controllers support dividing an interface into a master view and a detail view when in landscape orientation in a regular size class environment. If a detail view is available, the show detail segue will replace the current detail view.
  • Show. This segue really shines if the presenting view controller is in a navigation controller or a split view controller. The presented view controller is added or pushed onto the navigation stack of view controllers (in the split view controller's detail view if available), and a back button automatically appears in the navigation bar. If no navigation controller is available, it acts the same as a modal segue.
  • Present Modally. Presents the new view controller to cover the previous view controller — most commonly used to present a view controller that covers the entire screen on iPhone, or on iPad it's common to present it as a centered box that darkens the presenting view controller. Usually, if you had a navigation bar at the top or a tab bar at the bottom, those are covered by the modal view controller too.
  • Present as Popover. When run on an iPad, the new view controller appears in a popover, and tapping anywhere outside of this popover will dismiss it. On an iPhone, will present the new view controller modally over the full screen.
  • Custom. Allows you to implement your own custom segue and have control over its behavior.

So, each segue is a relation (transition) between different screens in your app, and those transitions can be fired upon a user's action or by using any other trigger (time trigger, or an action from a server):

  1. The easiest way to create a segue is to hold Ctrl and then drag it from the view controller or button to the view controller, which should be presented on the screen.
  2. Once you lift the mouse, a small popup is displayed, asking you to pick the action to be used. This action will define how the new view controller will be presented on the screen. We can use the Show option, but in other cases, some other options are better.
  3. Once we pick an action, a connection between our two view controllers will be added on the storyboard.

This link represents a relation between those two screens. If we add another segue, which will lead to another view controller, then we will have another link between these view controllers.

You can select the segue and add an identifier in the properties panel on the right. This identifier can be used to trigger a segue with code. For example, the selected segue has the following ID - showSecond

Another way to activate a segue is to use code and then add an action that will be fired upon using the Touch Up Inside event. Then in the function, you can activate the segue with the following code:

@IBAction func onDatailsClicked(_ sender: Any) {
    performSegue(withIdentifier: "showSecond", sender: sender)
}

You should create a segue with the showSecond identifier on the storyboard.

Now, we know how to transition from one scene to another, but we should pass data and make the user believe that those scenes are related.

Passing data forward

Passing Data Between View Controllers With Properties. A property is a variable that’s part of a class. Every instance of that class will have that property, and you can assign a value to it.

Here’s a view controller MainViewController with a property called text:

class MainViewController: UIViewController {
    var text:String = ""

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Let’s say this view controller is part of a navigation controller and we want to pass some data to a second view controller. Here’s the actual passing of the data. Add the following method to MainViewController:

@IBAction func onButtonTap() {
    let vc = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil)
    vc.text = "Hello there!"

    navigationController?.pushViewController(vc, animated: true)
}

Passing Data Between View Controllers Using Segues. Before you can pass data between two view controllers, you need to connect them with a segue. Then you need to make sure that each view controller has its own .swift file that you can connect it to through the Identity Inspector. Finally, you need to write Swift code in the .swift files of both view controllers to send and receive data.

First, when designing each screen, we make it dependent on some model (data). This model should be passed when the transition happens and once the view controller is presented, the data should appear. This way, the user thinks that the two screens are linked together. An example is a collection view and the details view, which displays extra info. These two screens can work independently, but when we pass data from one to the other, they are perceived as the same thing.

Now, let's open the code and jump straight to the view controller that we want to send data, when a specific segue is fired. In that controller, we have to override the following method:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let id = segue.identifier {
        switch id {
            case "showDetails":
                print("transfer the data");
            default:
                break;
        }
    }
}

In the preceding code, we detect which segue will be performed. We know which screen will be next. The new view controller that will be presented on the screen should accept the data. Usually, the view controller has a public property (properties) which should be set. You have two arguments. The first one is the segue object, which contains both view controllers. The sender object identifies the item that has triggered the segue. Both arguments are needed to distinguish between different logical scenarios and we can develop the app to act differently in different cases. Here is what the code should look like if we want to pass some data to the second view controller:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let id = segue.identifier {
        switch id {
            case "showDetails":
                guard let secondVC: SecondViewController = segue.destination as? SecondViewController else {
                    return
                }
                secondVC.receivedData = 42
            default:
                break;
        }
    }
}

or

override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 
    if let secondVC = segue.destination as? SecondViewController {
        secondVC.receivedData = myTextField.text ?? 42
    }
}

or

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    switch segue.identifier {
    case "showItem":
        if let row = tableView.indexPathForSelectedRow?.row {
            let item = items[row]
            let detailViewController = segue.destination as! DetailViewController
            detailViewController.item = item
        }
    default:
        preconditionFailure("Unexpected segue identifier.")
    }
}

The UIStoryboardSegue gives you three pieces of information: the source view controller (where the segue originates), the destination view controller (where the segue ends), and the identifier of the segue. The identifier lets you differentiate segues.

The receivedData property can be used freely in the viewDidLoad() method. We can pass a lot of information, which can be used in the next view controller. There is no limitation as to what type it should be. When the next view controller is activated, it will have access to the passed bits.

There’s another way to pass data through a segue that involves giving the segue a name. Then your code can run depending on the segue name. This method can be handy in case you have multiple segues linked to the same view controller.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 
    let secondVC = segue.destination as? SecondViewController 
    if segue.identifier == "oneSegue" {
        secondVC?.receivedData = myTextField.text ?? 42 
    }
    if segue.identifier == "twoSegue" {
        secondVC?.receivedData = 44
    } 
}

A slightly different problem is to pass data in the reverse direction. Let's try to understand why we need it first. When we do some actions in a child view controller, its nice to pass the data to the parent view controller. This approach will improve the module's design. Every view controller does it's own job and passes the model after some updates.

Passing data in the reverse direction

Using protocol. To use the delegation pattern, you’ll need to set up a delegate protocol that defines a list of all the methods that the delegate should implement. The first view controller would then adopt the protocol and define itself as the second view controller’s delegate.

Create the delegate protocol. The naming convention for the delegate of a class is to use the same name of the class with the suffix Delegate. Add the SecondViewControllerDelegate protocol to the SecondViewController.swift file.

protocol SecondViewControllerDelegate {
    func saveMovie(_ move: Movie)
}

Add a reference to the delegate in SecondViewController, and make it an optional.

var delegate: SecondViewControllerDelegate?

Now, to extract the data that the user has entered in the form, you’ll need to create outlets for each of the elements in the form.

In the touchSave method (SecondViewController) before calling the dismissMe method, create a object from the fields in the edit form, and pass it into the delegate method:

@IBAction func closeButton(_ sender: UIButton) { 
    let movieToSave = Movie(
        title: titleTextField.text!,
        rating: 7
    )
    delegate?.saveMovie(movieToSave)
    dismiss(animated: true, completion: nil)
}

During the segue, the FirstViewController class needs to tell the SecondViewController that it is the SecondViewController’s delegate. The problem is that because the segue was created in Interface Builder, the instantiation of the new view controller is managed automatically.

Fortunately, view controllers contain a prepareForSegue method that’s called after any new view controllers are instantiated but before the segue is performed.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let navController = segue.destination as? UINavigationController {
        if let secondViewController = navController.topViewController
        as? SecondViewController {
            secondViewController.delegate = self
        }
    }
}

...

extension FirstViewController: SecondViewControllerDelegate {
    func saveMovie(_ move: Movie) { ... }
}

Using delegate. Another way to pass data backward is to declare the first view controller as a delegate. Then define properties in both view controllers that hold the data you want to pass back from the second view controller.

class SecondViewController: UIViewController {
    @IBOutlet var myTextField: UITextField! 
    var sentText : String = ""
    var delegate : FirstViewController!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func closeButton(_ sender: UIButton) { 
        sentText = myTextField.text ?? "default value" 
        delegate.receivedText = sentText 
        dismiss(animated: true, completion: nil)
    }   
}

class FirstViewController: UIViewController {
    @IBOutlet var myLabel: UILabel!
    var receivedText : String = ""

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewWillAppear(_ animated: Bool) { 
        myLabel.text = receivedText
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 
        let vc = segue.destination as! SecondViewController 
        vc.sentText = self.receivedText
        vc.delegate = self
    } 
}

Using Notification Center. Yet another way to pass data between view controller files is through the notification center. Using the notification center to pass data can be especially useful when you need to share data between two or more view controllers at the same time, or if the view controllers are not connected by a segue. There’s a three-step process to using notification center:

  • Define a unique name for a notification center.
  • Add an observer to that notification center.
  • Send a notification to the observer and pass data.

To define a name for a notification center, you can choose any arbitrary name such as

static let notificationName = Notification.Name("myNotification")

To add a notification center observer involves defining a function to run when it receives a notification and defining the name of the notification center to observe. This can be done with a statement like this:

NotificationCenter.default.addObserver(self, selector: #selector(functionName(notification:)), 
name: notificationName, object: nil)

Where functionName is the name of your function to run when the notification is received, and notificationName is the name of the notification center you defined.

Finally, you need to send a notification and pass data at the same time. This can be done using this statement:

NotificationCenter.default.post(name: NSNotification.Name(rawValue:
"Notification Name"), object: dataSent)

Where "Notification Name" is the name you chose for the notification center, and dataSent is any data you wish to pass to a notification observer.

class FirstViewController: UIViewController {
    @IBOutlet var myLabel: UILabel!
    static let notificationName = Notification. Name("myNotification")

    @objc func onNotification(notification:Notification) {
        let data = notification.object
        let temp = String(describing: data!) 
        myLabel.text = temp
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), 
           name: FirstViewController.notificationName, object: nil)
    } 
}

class SecondViewController: UIViewController {
    @IBOutlet var myTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func closeButton(_ sender: UIButton) {
        let dataSent = myTextField.text 
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "myNotification"), object: dataSent) 
        dismiss(animated: true, completion: nil)
    } 
}

Pass in a closure. This alternative has similarities to the delegation pattern, but focuses on one closure rather than a list of methods in a protocol. The first view controller simply passes in a closure to the second view controller that the second view controller can then call before resigning itself.

Closures can be stored as variables to be called later. The following sets up an optional closure declaration in the second view controller class that could receive a Movie object and doesn’t return anything:

var saveMovie: ((Movie) -> Void)?

In the prepareForSegue method, the first view controller would then pass the complete saveMovie method into the second view controller as a closure:

secondViewController.saveMovie = { (_ movie: Movie) in 
    self.moviesManager.addMovie(moive)
}

Alternatively, the saveMovie method itself could be passed in:

secondViewController.saveMovie = saveMovie

The second view controller can now directly call the saveMovie method. Because closures capture variables from their original scope, when the second view controller calls the saveMovie method, it will automatically have access to variables it refers to in the first view controller’s scope. Because the closure is declared as an optional, it must be unwrapped when called:

saveMovie?(movieToSave)

Unwind segue. Passing data from the second view controller to the first view controller is a bit tricky. There is no easy way to know which view controller has opened the current view controller, thus when we want to define a segue back to a specific view controller, we should create a special function in that particular view controller.

In our case, we want to pass the selected item to the first screen. To do so, we have to add a new function that will handle the reverse transition:

class FirstViewController: UIViewController {
    var data = "Hello from FirstViewController"

    override func viewDidLoad() {
        super.viewDidLoad()
        print(data)
    }

    @IBAction func unwindToFirstScreen(sender: UIStoryboardSegue) {
        print(data)
    }
}

unwindToFirstScreen function will be triggered when the unwind segue is fired. We have to pass the data from the second view controller to the first view controller. To create this unwind segue, you have to start creating a segue with a Ctrl drag from the button on the second view controller and drop it to the exit point. Then, we have to pick the exact function that should be triggered. The functions in the list are part of the other view controllers.

Following is code for SecondViewController

class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print("SecondViewController")
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let firstVS = segue.destination as! FirstViewController
        firstVS.data = "Hello from SecondViewController"
    }
}

Loading a view controller via code

override func tableView(_ tableView: UITableView, 
    accessoryButtonTappedForRowWith indexPath: IndexPath) {
    let controller = storyboard!.instantiateViewController( withIdentifier: "ListDetailViewController")
        as! ListDetailViewController 
    controller.delegate = self
    let checklist = lists[indexPath.row] 
    controller.checklistToEdit = checklist
    navigationController?.pushViewController(controller, animated: true)
}

In this method, you create the view controller object and push it on to the navigation stack. This is roughly equivalent to what a segue would do behind the scenes. The view controller is embedded in a storyboard and you have to ask the storyboard object to load it.

The call to instantiateViewController(withIdentifier:) takes an identifier string, ListDetailViewController.

You still have to set this identifier on the navigation controller; otherwise the storyboard won't be able to find it. Open the storyboard and select the List Detail View Controller. Go to the Identity inspector and set Storyboard ID to ListDetailViewController.

How to push and present to ViewController programmatically?

Switch between VC is really important for every application. You can switch between VC with three options :

  • Segue
  • Present
  • Push

Present View Controller

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC")
self.present(vc, animated: true, completion: nil)

// or safe way

if let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC") as? MovieVC {
    present(vc, animated: true, completion: nil)
}

Push View Controller

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC")
navigationController?.pushViewController(vc, animated: true)

// or safe way

if let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC") as? MovieVC,
   let navigator = navigationController {
       navigator.pushViewController(vc, animated: true)
}

Useful links