Many iOS developers make serious architectural mistakes when passing data between view controllers.

There are many ways to do this, but they are not all good. Only a few of them are the best practices.

Quick and easy solutions like singletons often lead to severe problems you will discover only later.

If you want to be a skilled iOS developer, you need to know the architecture behind view controller communication.

In this article, I will show you the best practices to pass data both forward backward. We will also discuss some advanced techniques, and the solutions you should not use.

This article has been updated for iOS 12, Xcode 10 and both Swift 4 and Swift 5.

Architecting SwiftUI apps with MVC and MVVM GET THE FREE BOOK NOW

Contents



Section 1 The difference between best and weak practices

Section 2 Passing data forward

Section 3 Passing data backward

Section 4 Advanced techniques

Section 5 Wrong techniques

Section 1: The difference between best and weak practices Passing data between view controllers might sound like a trivial task. But when considering that any real iOS app will have many view controllers, communication becomes a crucial part. Getting it wrong can lead to hard to fix bugs.

What happens when you get view controller communication wrong

I worked with many clients and also built my apps. I know first-hand how bad practices can derail a project.

When Apple first published the iOS SDK in 2008, I made a little game for the iPhone. I read some Apple guides and started coding my app from there.

Like any other iOS developer, I soon came to a roadblock: view controller communication.

I was coming from Mac development where there were no view controllers. It was a new concept for me, and I was not well-versed in software architecture and best practices (they don’t teach those in universities).

So I did what I could with my little experience.

Some years later, I wanted to improve my little game and add some new features. At that point, my experience was much more prominent.

When I opened the project, and I looked at the architecture of the app, I wondered:

WTF was I thinking when I wrote this?

You see, sometimes it is easy to look at someone else’s code and think they are bad developers. The truth is that everybody goes through an initial phase of a poor understanding of the platform.

Don’t be a copy and paste developer

What is the difference between the few developers that become highly skilled and all the other copy-and-paste developers?

Good developers know they lack knowledge and take the time to grow it.

to grow it. Bad developers never bother. They go on with the little they know, trying to hack together something and calling it a day.

Do you want to be a proficient iOS developer? Then you need to understand the ideas behind view controller communication.

In the past, I wrote about the MVC pattern in iOS and how iOS apps are structured. While in those articles I treated view controller as separate independent entities, they are not.

In any non-trivial app, view controllers need to communicate and send data back and forth.

There are many ways to enable communication between view controllers. If you look at this question on Stack Overflow, you can find pretty much all of them.

But which ones are bad?

Here are all the ways in which you can pass data between view controllers:

when a segue is performed

triggering transitions programmatically in your code

through the state of the app

using singletons

using the app delegate

assigning a delegate to a view controller

through unwind segues

referencing a view controller directly

using the user defaults

through notifications

with Swift closures

But only a few of them are the best practices.

For example, you should not reference view controllers directly, which was the mistake I made in my little game. Nor use notifications, which sank a project, which I had to rescue, for one of my clients.

Recognizing good and bad practices

So, what makes some techniques better than others?

Best practices respect the principles of good software architecture and common design patterns.

Recently, I checked again Apple’s guide on view controllers. I was surprised to find that the correct information was in there all the time.

But as I often mention, Apple’s documentation is a bit cryptic, and sometimes even wrong (yes, even Apple makes mistakes). When I read that guide years ago, I could not absorb all the concepts.

So let’s have a look at how to pass data between view controllers in more detail.

In a complex app, there might be many paths that the user can follow. What matters though is that view controllers come on the screen one after another.

So, there are only two directions in which data can flow.

forward , to any new view controller that comes on the screen; and

, to any new view controller that comes on the screen; and backwards, to a view controller that was previously on display and to which the user goes back at some point.

The two are not equivalent. Each one has its techniques.

We are going to see all of them, applied to real-world situations in a small sample app to organize contacts. You can find the final version of the code on GitHub.

Despite their simplicity, these view controllers cover every possible case.

Section 2: Passing data forward Passing data forward happens every time a new view controller comes on the screen. This can happen through a segue or programmatically. Moreover, some view controller containers can make things more complicated.

Passing data forward through a show segue

The most common situation in which you need to pass data forward is when a transition happens through a storyboard segue.

There are two different types of forward segues. The first one we will explore is a show segue in a navigation controller. This produces the typical drill-down navigation of iOS apps.

This is often triggered by selecting a row in a table view.

Before we can pass data between view controllers, we need a Swift structure to represent a contact.

1 2 3 4 5 6 7 struct Contact { let photo : UIImage let name : String let position : String let email : String let phone : String }

The view controllers in a storyboard do not know each other. The only get connected for a short time when a segue happens.

This is precisely the channel through which view controllers communicate.

When a transition is triggered, and a segue is performed, the prepare ( for : sender : ) method is called in the source view controller.

method is called in the source view controller. The segue, which is an object, is passed as a parameter to that method. Through its destination property, you can access the destination view controller.

property, you can access the destination view controller. The destination view controller needs to declare a public stored property to receive data.

First of all, let’s set up the destination view controller.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? override func viewDidLoad ( ) { super . viewDidLoad ( ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } }

We can’t write a custom initializer for a view controller in a storyboard. Since it can only receive data after initialization, the contact property needs to be optional.

We now need a view controller with a list of contacts.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class ContactsViewController : UITableViewController { let contacts : [ Contact ] = [ Contact ( photo : imageLiteral ( resourceName : "Madison" ) , name : "Madison Thompson" , position : "Sales, Gale Foods" , email : "madison@galefoods.com" , phone : "4-(968) 705-1370" ) , Contact ( photo : imageLiteral ( resourceName : "Tyler" ) , name : "Tyler Porter" , position : "Software developer, Prophecy" , email : "tyles@propehcy.com" , phone : "2-(513) 832-7517" ) , Contact ( photo : imageLiteral ( resourceName : "Katherine" ) , name : "Katherine Price" , position : "Marketing, Golden Roads" , email : "katherine@goldenroads.com" , phone : "1-(722) 844-1495" ) , Contact ( photo : imageLiteral ( resourceName : "Gary" ) , name : "Gary Edwards" , position : "Web Developer, Bluewares" , email : "gary@bluewares.com" , phone : "9-(687) 559-3525" ) , Contact ( photo : imageLiteral ( resourceName : "Rebecca" ) , name : "Rebecca Rogers" , position : "HR, Globaviations" , email : "rebecca@globaviations.com" , phone : "3-(710) 249-5471" ) ] override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } } class ContactCell : UITableViewCell { @ IBOutlet var photoImageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? }

For simplicity, the ContactsViewController acts as a data source for its table view, but that’s not the best practice. You can read more about that in my article on UITableView

Finally, we pass the selected contact from to the DetailViewController in the prepare(for:sender:) method of ContactsViewController:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class ContactsViewController : UITableViewController { let contacts : [ Contact ] = [ Contact ( photo : imageLiteral ( resourceName : "Madison" ) , name : "Madison Thompson" , position : "Sales, Gale Foods" , email : "madison@galefoods.com" , phone : "4-(968) 705-1370" ) , Contact ( photo : imageLiteral ( resourceName : "Tyler" ) , name : "Tyler Porter" , position : "Software developer, Prophecy" , email : "tyles@propehcy.com" , phone : "2-(513) 832-7517" ) , Contact ( photo : imageLiteral ( resourceName : "Katherine" ) , name : "Katherine Price" , position : "Marketing, Golden Roads" , email : "katherine@goldenroads.com" , phone : "1-(722) 844-1495" ) , Contact ( photo : imageLiteral ( resourceName : "Gary" ) , name : "Gary Edwards" , position : "Web Developer, Bluewares" , email : "gary@bluewares.com" , phone : "9-(687) 559-3525" ) , Contact ( photo : imageLiteral ( resourceName : "Rebecca" ) , name : "Rebecca Rogers" , position : "HR, Globaviations" , email : "rebecca@globaviations.com" , phone : "3-(710) 249-5471" ) ] override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { guard let detailViewController = segue . destination as ? DetailViewController , let index = tableView . indexPathForSelectedRow ? . row else { return } detailViewController . contact = contacts [ index ] } }

You can run the app and see that it works.

Keep in mind that prepare(for:sender:) gets called for all segues going out from a view controller. In this case, we only have one, but in a real app, you can have more.

To know which segue was triggered, you have two options:

use optional binding like in the example above, to check which view controller comes next; or

give an identifier to the segue, and then check the identifier property of UIStoryboardSegue .

Passing data forward through a modal segue

The other forward segue you find in iOS apps is the present modally segue. In iOS, any screen that requires user input is usually presented this way.

This is again a forward segue, so it works as the show segue we saw above.

But often, you find an obstacle on your way.

To get a navigation bar at the top of the screen, you need to place your destination view controller inside a navigation controller.

Now, there is a navigation controller between the source and the destination. The mechanism stays the same though.

The destination view controller still receives its data through a stored property.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class EditContactViewController : UITableViewController { @ IBOutlet weak var imageView : UIImageView ! @ IBOutlet weak var nameTextField : UITextField ! @ IBOutlet weak var positionTextField : UITextField ! @ IBOutlet weak var emailTextField : UITextField ! @ IBOutlet weak var phoneTextField : UITextField ! var contact : Contact ? override func viewDidLoad ( ) { super . viewDidLoad ( ) imageView . image = contact ? . photo nameTextField . text = contact ? . name positionTextField . text = contact ? . position emailTextField . text = contact ? . email phoneTextField . text = contact ? . phone } }

The source view controller instead needs to dig through the navigation controller.

To do that, we:

cast the destination of the segue to UINavigationController ;

of the segue to ; get its root view controller through the viewControllers property.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? override func viewDidLoad ( ) { super . viewDidLoad ( ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let navigationController = segue . destination as ? UINavigationController , let editContactViewController = navigationController . viewControllers . first as ? EditContactViewController { editContactViewController . contact = contact } } }

In theory, we could create a UINavigationController subclass that passes data to its children. But that would not be a practical solution for such a small task.

Later, we will see a better, more generic solution in the advanced techniques section.

Passing data forward without a segue

Sometimes, you might connect view controllers programmatically instead of using a segue.

This happens when the destination view controller:

is in a nib file;

creates its view hierarchy in code.

In this case, the passage of data is straightforward. Since the source view controller instantiates the destination view controller, it has a direct reference to the latter.

I usually recommend using storyboards in any iOS app, since they have many advantages. But not using storyboards has one advantage.

View controllers that don’t come from a storyboard can have custom initializers.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class EditContactViewController : UITableViewController { @ IBOutlet weak var imageView : UIImageView ! @ IBOutlet weak var nameTextField : UITextField ! @ IBOutlet weak var positionTextField : UITextField ! @ IBOutlet weak var emailTextField : UITextField ! @ IBOutlet weak var phoneTextField : UITextField ! let contact : Contact init ( contact : Contact ) { self . contact = contact super . init ( nibName : nil , bundle : nil ) } required init ? ( coder aDecoder : NSCoder ) { fatalError ( "init(coder:) has not been implemented" ) } override func viewDidLoad ( ) { super . viewDidLoad ( ) imageView . image = contact . photo nameTextField . text = contact . name positionTextField . text = contact . position emailTextField . text = contact . email phoneTextField . text = contact . phone } }

This removes optionals from the view controller properties, which means less unwrapping code.

Since there is no segue, the source controller instantiates the destination directly. The prepare(for:sender:) method is also not called.

Instead, we handle user interaction in

an action method for buttons, or

the tableView ( _ : didSelectRowAt : ) method of UITableViewDelegate when using table views.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? override func viewDidLoad ( ) { super . viewDidLoad ( ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } @ IBAction func edit ( _ sender : Any ) { guard let contact = contact else { return } let editViewController = EditContactViewController ( contact : contact ) let navigationViewController = UINavigationController ( rootViewController : editViewController ) show ( navigationViewController , sender : nil ) } }

Passing data between view controllers inside a tab bar controller

Another common container in iOS apps is the tab bar controller. This allows the user to switch between different tabs using a tab bar at the bottom of the screen.

Sometimes, you need to update one tab as a consequence of a user action in another tab. But view controllers in different tabs do not have a direct channel of communication.

Switching between view controllers in different tabs does not trigger any segue or any event inside view controllers.

You can still detect tab switching adding a delegate to the tab bar controller, but that’s not a good solution for view controller communication.

In our example app, we can add a feature to mark a contact as favorite directly in the first tab. Of course, when the user switches to the second tab, the list of favorite contacts needs to be up to date.

In this case, view controllers communicate indirectly through the state of the app.

The state of the app is usually contained inside a model controller.

The first view controller updates the state of the app as a consequence of user interaction.

When the second view controller comes on screen, it fetches the updated data from the model controller and updates its interface.

In iOS apps, a tab bar controller is usually the initial view controller, which is reachable from the app delegate.

Let’s start by creating a model controller that contains the state of the app.

1 2 3 4 5 6 7 8 9 10 11 class StateController { let contacts : [ Contact ] = [ Contact ( photo : imageLiteral ( resourceName : "Madison" ) , name : "Madison Thompson" , position : "Sales, Gale Foods" , email : "madison@galefoods.com" , phone : "4-(968) 705-1370" ) , Contact ( photo : imageLiteral ( resourceName : "Tyler" ) , name : "Tyler Porter" , position : "Software developer, Prophecy" , email : "tyles@propehcy.com" , phone : "2-(513) 832-7517" ) , Contact ( photo : imageLiteral ( resourceName : "Katherine" ) , name : "Katherine Price" , position : "Marketing, Golden Roads" , email : "katherine@goldenroads.com" , phone : "1-(722) 844-1495" ) , Contact ( photo : imageLiteral ( resourceName : "Gary" ) , name : "Gary Edwards" , position : "Web Developer, Bluewares" , email : "gary@bluewares.com" , phone : "9-(687) 559-3525" ) , Contact ( photo : imageLiteral ( resourceName : "Rebecca" ) , name : "Rebecca Rogers" , position : "HR, Globaviations" , email : "rebecca@globaviations.com" , phone : "3-(710) 249-5471" ) ] var favorites : [ Contact ] = [ ] }

A real state controller would probably have a more complex interface to add and remove favorites, and would probably store its data on the disk. But let’s keep things simple.

Both tabs show a list of contacts, so we can put a ContactsViewController in each one. We just need to change it a bit, so it gets its data from the StateController.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class ContactsViewController : UITableViewController { var stateController : StateController ? var favoritesOnly = false var contacts : [ Contact ] { if favoritesOnly { return stateController ? . favorites ? ? [ ] } return stateController ? . contacts ? ? [ ] } override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { guard let detailViewController = segue . destination as ? DetailViewController , let index = tableView . indexPathForSelectedRow ? . row else { return } detailViewController . contact = contacts [ index ] } }

If you feel there is too much going on with optionals here, be sure to grab my guide to Swift optionals.

To favorite a contact, we can use table view row actions.

1 2 3 4 5 6 7 8 9 override func tableView ( _ tableView : UITableView , editActionsForRowAt indexPath : IndexPath ) -> [ UITableViewRowAction ] ? { let favoriteAction = UITableViewRowAction ( style : . default , title : "Favorite" ) { [ weak self ] ( _ , indexPath ) in guard let favorite = self ? . stateController ? . contacts [ indexPath . row ] else { return } self ? . stateController ? . favorites . append ( favorite ) } return [ favoriteAction ] }

Finally, we need to update the UI of the view controller every time it comes on the screen. In this way, the favorites are always going to be up to date.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class ContactsViewController : UITableViewController { var stateController : StateController ? var favoritesOnly = false var contacts : [ Contact ] { if favoritesOnly { return stateController ? . favorites ? ? [ ] } return stateController ? . contacts ? ? [ ] } override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) tableView . reloadData ( ) } override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { guard let detailViewController = segue . destination as ? DetailViewController , let index = tableView . indexPathForSelectedRow ? . row else { return } detailViewController . contact = contacts [ index ] } }

The only thing left is giving to both instances of ContactsViewController a shared StateController instance. We do so in the AppDelegate.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class AppDelegate : UIResponder , UIApplicationDelegate { var window : UIWindow ? let stateController = StateController ( ) func application ( _ application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ UIApplication . LaunchOptionsKey : Any ] ? ) -> Bool { guard let tabBarController = window ? . rootViewController as ? UITabBarController , let viewControllers = tabBarController . viewControllers else { return true } for ( index , viewController ) in viewControllers . enumerated ( ) { if let navigationController = viewController as ? UINavigationController , let contactsViewController = navigationController . viewControllers . first as ? ContactsViewController { contactsViewController . stateController = stateController contactsViewController . favoritesOnly = index == 1 } } return true } }

Again, each tab in the tab bar controller contains a navigation controller, so we have to dig through those to get to the two ContactsViewController instances.

Section 3: Passing data backward Passing data backward in an iOS app is as important as moving it forward. Users often go back to a previous screen they visited. As the user interacts with your app, you have to keep those previous screens updated. That does not happen automatically, so there are different techniques you can use.

Passing data back through an unwind segue

If you use storyboards in your app and navigate forward using segues, to keep things consistent, you use unwind segues to go back.

Unwind segues are commonly used to reverse modal presentation. In navigation controllers, there is no need for unwind segues since the container takes care of everything.

But you can also use an unwind segue in a navigation controller if you want to jump back more than one screen.

Unwind segues have many moving parts, so they can be a bit confusing.

First of all, you cannot directly connect a control, e.g., a button, to an unwind segue. You first need to create an unwind action inside the view controller you want to go back to.

In an unwind segue, the source is the view controller currently on screen, and the destination is the previous view controller.

At first sight, this does not look like a big problem. When an unwind segue is triggered, the prepare(for:sender:) is called in the source view controller as you would expect.

But since we are going back, it’s not a good practice to reference the previous view controller.

It’s common for a view controller to know which view controllers come after. But a view controller should not know the ones which come before.

So, here the steps are different:

In its prepare ( for : sender : ) method, the source view controller stores the data it wants to pass back in a public property.

method, the source view controller stores the data it wants to pass back in a public property. After that, the unwind action is called in the destination view controller. This method also gets the UIStoryboardSegue instance as a parameter. Through the segue, the destination reads the data stored in the property of the source.

In our sample app, we first store the edited data for a contact in the contact property when prepare(for:sender:) gets called.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class EditContactViewController : UITableViewController { @ IBOutlet weak var imageView : UIImageView ! @ IBOutlet weak var nameTextField : UITextField ! @ IBOutlet weak var positionTextField : UITextField ! @ IBOutlet weak var emailTextField : UITextField ! @ IBOutlet weak var phoneTextField : UITextField ! var contact : Contact ? override func viewDidLoad ( ) { super . viewDidLoad ( ) imageView . image = contact ? . photo nameTextField . text = contact ? . name positionTextField . text = contact ? . position emailTextField . text = contact ? . email phoneTextField . text = contact ? . phone } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let photo = imageView . image , let name = nameTextField . text , let position = positionTextField . text , let email = emailTextField . text , let phone = phoneTextField . text { contact = Contact . init ( photo : photo , name : name , position : position , email : email , phone : phone ) } } }

Then, in the unwind action in the destination view controller, we read the contact data.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let navigationController = segue . destination as ? UINavigationController , let editContactViewController = navigationController . viewControllers . first as ? EditContactViewController { editContactViewController . contact = contact } } @ IBAction func cancel ( _ unwindSegue : UIStoryboardSegue ) { } @ IBAction func save ( _ unwindSegue : UIStoryboardSegue ) { if let editViewController = unwindSegue . source as ? EditContactViewController { contact = editViewController . contact } } }

Finally, the UI of the view controller gets updated by the viewWillAppear(_:) method we already implemented.

Passing data back through the shared state of the app

As we have seen above, model controllers can keep track of the current state of the app.

The state of the app is a clear channel through which view controllers can communicate with each other in any direction.

We have already seen an example above, so I won’t repeat it here.

All you need to do is pass a shared model controller instance to the destination view controller when moving forward.

That view controller can then make all the updates it needs to the state of the app.

When the user goes back to the previous view controller, this can read the new state in its viewWillAppear ( _ : ) method, as we saw in the example above.

Passing data back using a delegate

Sometimes the techniques we have seen are still not enough.

The data you want to pass back might be temporary and not belong to the shared state of the app.

When the user goes back in a navigation controller, no segue is triggered.

You might want communication to happen at any moment and not only when a transition occurs.

In these cases, we need a direct connection between the current view controller and the previous one. As I explained above though view controllers should not know anything about previous ones.

The solution is delegation.

Delegation allows us to create a link to a previous view controller without knowing its type. We do so with a protocol that defines the interface with which we need to interact.

This is how delegation works:

We define a delegate protocol to define an interface with which the source view controller will interact.

We use this protocol as the type of a stored property. This creates a link to the destination view controller without having to specify its type.

The destination view controller adopts the delegate protocol and implements its interface. Them, it passes a reference to itself during a forward segue.

The source view controller can then interact with the destination through the interface defined by the protocol.

In our app, when the user saves the changes to a contact, the EditContactViewController passes data back to the DetailViewController through the segue.

But this data does not yet get passed back to the ContactViewController.

To create a link back from the DetailViewController we define a delegate protocol with a method to update a contact.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 protocol DetailViewControllerDelegate : AnyObject { func update ( _ contact : Contact ) } class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? weak var delegate : DetailViewControllerDelegate ? override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let navigationController = segue . destination as ? UINavigationController , let editContactViewController = navigationController . viewControllers . first as ? EditContactViewController { editContactViewController . contact = contact } } @ IBAction func cancel ( _ unwindSegue : UIStoryboardSegue ) { } @ IBAction func save ( _ unwindSegue : UIStoryboardSegue ) { if let editViewController = unwindSegue . source as ? EditContactViewController { contact = editViewController . contact if let contact = contact { delegate ? . update ( contact ) } } } }

Any delegate property needs to be weak to avoid strong reference cycles. See this article for more details on weak and strong references.

In the code above, I pass the data back in the unwind action, but you can do that at any moment. The advantage of delegation is that the link is always available.

The ContactsViewController then adopts the DetailViewControllerDelegate and establishes the link when moving forward.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class ContactsViewController : UITableViewController { var stateController : StateController ? var favoritesOnly = false var contacts : [ Contact ] { if favoritesOnly { return stateController ? . favorites ? ? [ ] } return stateController ? . contacts ? ? [ ] } override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) tableView . reloadData ( ) } override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { guard let detailViewController = segue . destination as ? DetailViewController , let index = tableView . indexPathForSelectedRow ? . row else { return } detailViewController . contact = contacts [ index ] detailViewController . delegate = self } override func tableView ( _ tableView : UITableView , editActionsForRowAt indexPath : IndexPath ) -> [ UITableViewRowAction ] ? { let favoriteAction = UITableViewRowAction ( style : . default , title : "Favorite" ) { [ weak self ] ( _ , indexPath ) in guard let favorite = self ? . stateController ? . contacts [ indexPath . row ] else { return } self ? . stateController ? . favorites . append ( favorite ) } return [ favoriteAction ] } } extension ContactsViewController : DetailViewControllerDelegate { func update ( _ contact : Contact ) { stateController ? . update ( contact ) } }

In the update(_:) delegate method, the ContactsViewController updates the contact in the StateController. We still don’t have that method, so here is a simple implementation.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class StateController { private ( set ) var contacts : [ Contact ] = [ Contact ( photo : imageLiteral ( resourceName : "Madison" ) , name : "Madison Thompson" , position : "Sales, Gale Foods" , email : "madison@galefoods.com" , phone : "4-(968) 705-1370" ) , Contact ( photo : imageLiteral ( resourceName : "Tyler" ) , name : "Tyler Porter" , position : "Software developer, Prophecy" , email : "tyles@propehcy.com" , phone : "2-(513) 832-7517" ) , Contact ( photo : imageLiteral ( resourceName : "Katherine" ) , name : "Katherine Price" , position : "Marketing, Golden Roads" , email : "katherine@goldenroads.com" , phone : "1-(722) 844-1495" ) , Contact ( photo : imageLiteral ( resourceName : "Gary" ) , name : "Gary Edwards" , position : "Web Developer, Bluewares" , email : "gary@bluewares.com" , phone : "9-(687) 559-3525" ) , Contact ( photo : imageLiteral ( resourceName : "Rebecca" ) , name : "Rebecca Rogers" , position : "HR, Globaviations" , email : "rebecca@globaviations.com" , phone : "3-(710) 249-5471" ) ] var favorites : [ Contact ] = [ ] func update ( _ contact : Contact ) { for ( index , old ) in contacts . enumerated ( ) { if old . name == contact . name { contacts [ index ] = contact break } } } }

Yes, I know that this method does not work if the user changes the name of a contact, but it’s good enough for our example.

Section 4: Advanced techniques The techniques we saw in the previous sections are the standard best practices for view controller communication in iOS. There are also a couple of alternative techniques you can use. These are more advanced, so you don’t need to adopt them straight away. But when you are ready, they can be useful.

Replacing delegation with Swift closures

Some developers use Swift closures to pass data backward between view controllers.

This technique is similar to delegation but more flexible. That’s also the reason why I usually recommend not to use it.

When using delegation, you specify the interface of the destination view controller in a protocol. Using closures, you can define that interface through stored properties containing closures.

The source view controller executed these closures to interact with a previous view controller. This is the same as calling methods on a delegate.

The destination view controller also establishes a link when a forward transition happens. But in this case, instead of passing forward a reference to itself, it passes a closure.

As an example, we can replace delegation in the DetailViewController with a closure.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? var updateContactClosure : ( ( Contact ) -> Void ) ? override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let navigationController = segue . destination as ? UINavigationController , let editContactViewController = navigationController . viewControllers . first as ? EditContactViewController { editContactViewController . contact = contact } } @ IBAction func cancel ( _ unwindSegue : UIStoryboardSegue ) { } @ IBAction func save ( _ unwindSegue : UIStoryboardSegue ) { if let editViewController = unwindSegue . source as ? EditContactViewController { contact = editViewController . contact if let contact = contact { updateContactClosure ? ( contact ) } } } }

As you can see, the code looks pretty much the same as before. The only difference is that we lost the protocol and the stored property has now a function type.

When the forward segue is triggered, the ContactsViewController injects a closure into that stored property.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class ContactsViewController : UITableViewController { var stateController : StateController ? var favoritesOnly = false var contacts : [ Contact ] { if favoritesOnly { return stateController ? . favorites ? ? [ ] } return stateController ? . contacts ? ? [ ] } override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) tableView . reloadData ( ) } override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { guard let detailViewController = segue . destination as ? DetailViewController , let index = tableView . indexPathForSelectedRow ? . row else { return } detailViewController . contact = contacts [ index ] detailViewController . updateContactClosure = { [ weak self ] contact in self ? . stateController ? . update ( contact ) } } override func tableView ( _ tableView : UITableView , editActionsForRowAt indexPath : IndexPath ) -> [ UITableViewRowAction ] ? { let favoriteAction = UITableViewRowAction ( style : . default , title : "Favorite" ) { [ weak self ] ( _ , indexPath ) in guard let favorite = self ? . stateController ? . contacts [ indexPath . row ] else { return } self ? . stateController ? . favorites . append ( favorite ) } return [ favoriteAction ] } }

The code in the closure is the one that was in the delegate method.

Notice also that the closure contains a reference to self. So, like delegation, using closures still creates a link between the two view controllers.

This approach is a bit more concise than delegation. But using closures also presents several features/disadvantages that delegation does not have.

Giving a good name to stored properties that contain closures is not as easy as giving proper names to methods.

If you need more than one closure to communicate with the previous view controller, you need a stored property for each one of them. With delegation, the whole interface is segregated inside a protocol, and you need only one delegate property.

Delegation forces a single channel of communication. Multiple closure properties are more flexible and can point to different view controllers. You can’t be sure where they go through. You pay for the flexibility with code that is harder to understand.

In my opinion, closures work better as callbacks for asynchronous tasks, like network requests or animations. Delegation is a better solution for view controller communication.

Removing repeated code by using an injecting segue

You might have noticed that in the various prepare(for:segue:) methods, we had to cast view controller instances to the appropriate class before we could pass any data to them.

That code becomes more tedious when we add containers like navigation controllers.

It would be better if we could get rid of such code, because:

It needs to be repeated in every view controller. Since an app may have many navigation paths that lead to a single view controller, every preceding view controller needs the same code.

While it’s acceptable for a view controller to reference the next ones, this makes our storyboard less flexible. When you rearrange the scenes in a storyboard, you also have to change the code to pass data forward.

Some time ago I discovered a technique to remove all that code from view controllers and move it in a single location.

I have been teaching this in my courses for a while. I might not be the first one to use it, but I haven’t seen anyone use it yet.

The trick relies on the fact that a segue is an object. That means we can extend the UIStoryboardSegue class and put there all the code that casts view controller instances.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 extension UIStoryboardSegue { func forward ( _ contact : Contact ? , to destination : UIViewController ) { if let navigationController = destination as ? UINavigationController { let root = navigationController . viewControllers [ 0 ] forward ( contact , to : root ) } if let detailViewController = destination as ? DetailViewController { detailViewController . contact = contact } if let editContactViewController = destination as ? EditContactViewController { editContactViewController . contact = contact } } }

Notice that this method also descends recursively into containers.

The code above is a bit longer than the one we wrote in view controllers, but you need to write it only once. Any view controller that passes a contact forward can do so in a single line of code.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { segue . forward ( contact , to : segue . destination ) } @ IBAction func cancel ( _ unwindSegue : UIStoryboardSegue ) { } @ IBAction func save ( _ unwindSegue : UIStoryboardSegue ) { if let editViewController = unwindSegue . source as ? EditContactViewController { contact = editViewController . contact if let contact = contact { delegate ? . update ( contact ) } } } }

Any reference to the next view controller is gone, making this view controller wholly decoupled.

Section 5: Wrong techniques We have seen the best practices for view controller communication. Unfortunately, I many projects, I saw many of the wrong ones too. In this section, we will look at which one they are, and why you should not use them.

Do not reference the previous view controller directly

In theory, delegation could be substituted by a direct reference to a previous view controller. After all, that’s what delegation is anyway.

So, why bother with a delegate protocol?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class DetailViewController : UITableViewController { @ IBOutlet var imageView : UIImageView ? @ IBOutlet var nameLabel : UILabel ? @ IBOutlet var positionLabel : UILabel ? @ IBOutlet var emailButton : UIButton ? @ IBOutlet var phoneButton : UIButton ? var contact : Contact ? weak var contactsViewController : ContactsViewController ? override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) imageView ? . image = contact ? . photo nameLabel ? . text = contact ? . name positionLabel ? . text = contact ? . position emailButton ? . setTitle ( contact ? . email , for : . normal ) phoneButton ? . setTitle ( contact ? . phone , for : . normal ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { segue . forward ( contact , to : segue . destination ) } @ IBAction func cancel ( _ unwindSegue : UIStoryboardSegue ) { } @ IBAction func save ( _ unwindSegue : UIStoryboardSegue ) { if let editViewController = unwindSegue . source as ? EditContactViewController { contact = editViewController . contact if let contact = contact { contactsViewController ? . update ( contact ) } } } }

The ContactsViewController then needs to provide the required interface.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class ContactsViewController : UITableViewController { var stateController : StateController ? var favoritesOnly = false var contacts : [ Contact ] { if favoritesOnly { return stateController ? . favorites ? ? [ ] } return stateController ? . contacts ? ? [ ] } func update ( _ contact : Contact ) { stateController ? . update ( contact ) } override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) tableView . reloadData ( ) } override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return contacts . count } override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let contact = contacts [ indexPath . row ] let cell = tableView . dequeueReusableCell ( withIdentifier : "ContactCell" , for : indexPath ) as ! ContactCell cell . photoImageView ? . image = contact . photo cell . nameLabel ? . text = contact . name cell . positionLabel ? . text = contact . position return cell } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { guard let detailViewController = segue . destination as ? DetailViewController , let index = tableView . indexPathForSelectedRow ? . row else { return } detailViewController . contact = contacts [ index ] detailViewController . delegate = self } override func tableView ( _ tableView : UITableView , editActionsForRowAt indexPath : IndexPath ) -> [ UITableViewRowAction ] ? { let favoriteAction = UITableViewRowAction ( style : . default , title : "Favorite" ) { [ weak self ] ( _ , indexPath ) in guard let favorite = self ? . stateController ? . contacts [ indexPath . row ] else { return } self ? . stateController ? . favorites . append ( favorite ) } return [ favoriteAction ] } }

This is the same code we wrote for delegation, minus the protocol. But it has many more problems.

Referencing the previous view controller class creates more coupling than necessary between the view controllers. Changes in the interface of the destination produce changes also in the source.

In an iOS app, a view controller is usually reached through more than one path. This means that we need a specific property for every view controller that might come before the current one.

Changes to the navigation flow in the storyboard would break our code.

As Apple says in its view controllers guide:

Always use a delegate to communicate information back to other controllers. Your content view controller should never need to know the class of the source view controller or any controllers it doesn’t create.

Do not use a singleton to share the app state

Singletons are an abused programming pattern. Especially in iOS, singletons get used for anything and everything.

The reason is that singletons are very easy to create and use. And even Apple endorses them in their documentation.

It looks like a perfect solution, but it’s not.

On the surface, a singleton looks like using the state of the app.

But a singleton can be accessed from anywhere in the app, not only from the view controllers that have an explicit dependency. Singletons introduce a lot of coupling and make objects harder to test and reuse.

This isn’t just my opinion, by the way. You can search on Google for “singleton antipattern” for more information about why singletons are bad. For example, look at this Stack Overflow question

Do not use the app delegate

Using the app delegate to pass data or store the app’s state is the same as using a singleton.

Although the app delegate is not technically a singleton, it can be considered one.

This is because you can access it through the shared UIApplication instance, which is a singleton.

In short, this code should never appear anywhere in your project:

1 2 let appDelegate = UIApplication . shared . delegate // Do something with the app delegate

Do not use the iOS user defaults

In iOS, the UserDefaults store user preferences that need to survive between app launches.

Anything stored in the user defaults stays there until you remove it explicitly, so it is not a mechanism to pass data between objects.

Also, you can only store simple data types in the user defaults, in the form of property lists. This means that you need to convert any custom type before you can put it there.

Finally, this is again like using a singleton. While you can create separate UserDefaults instances, what usually happens is that developers use the shared instance, which is a singleton.

In general, your app should access the user defaults through a single point, which is usually a custom shared model controller as I showed above in this article.

Do not use notifications

Notifications in iOS give you a channel through which some code can send a message to other objects to which it does not have a direct reference.

I have seen many developers use notifications to pass data between view controllers. That’s not what notifications are for.

Notifications create indirection and make your code harder to follow. When you post a notification you cannot be sure which objects will receive it, nor in which order. This often leads to unexpected behavior.

I have seen projects crippled by bugs caused by notifications going to the wrong objects.

Notifications can be useful sometimes. But you should use them sparingly, and only when necessary.

We have seen many direct communication channels between view controllers. There is no point in using an indirect one.

Should view controllers know about each other at all?

You might have noticed that many of the best practices lead view controllers to know about other ones, at least going forward.

But should view controllers know about each other?

The point of storyboards is to be able to change the flow of an app at any moment without changing its code.

Moreover, many paths can lead to a single view controller. We often end with a lot of coupling between view controller classes and repeated code.

The single responsibility of a view controller is to manage a single screen.

If we take this literally, a view controller should not:

know about other view controllers, and

contain code that manages the navigation flow.

In the previous section, I showed an advanced technique to get rid of coupling.

In more advanced design patterns, view controller communication is instead handled by coordinators.

Coordinators have direct access to any view controller, and they can also take care of dependency injection. This relieves view controllers from passing dependencies to each other.

One of such design patterns is VIPER. I also use coordinators in my Lotus MVC Pattern.

But those are advanced design patterns. The techniques I listed in this article are reliable and widely used among iOS developers.

I used them for many projects. You can rely on them until you are ready to adopt more advanced practices. Often, a good enough solution that works now is better than a perfect one that takes time to learn.

And in the end, there are no perfect solutions.

Conclusions

There are many ways to pass data between view controllers.

Only some of them are best practices though. Others might look convenient at first, but create problems later.

Pick the correct ones, and move on. There are other things to focus on when making apps.