If your iOS app consists of more than one screen, you’ll almost certainly have to pass data from one screen to another. MVC or not, this is easy to do when you present a new view controller. You simply set a property in your view controller or some other class associated with the destination.
But what about when you dismiss a view controller? And you want to pass data backward to the previous view controller?
In this article, we’ll look at two solutions to pass data backward:
- Delegation
- Data store
The old way – Delegation
Typically, you use the delegation pattern to accomplish the task of passing data back when you dismiss a view controller. Let’s walk through an example in which the Parent
presents the Child
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
protocol ChildViewControllerDelegate { func childViewControllerWillDismiss(childViewController: ChildViewController) } class ChildViewController: UIViewController { var delegate: ChildViewControllerDelegate? } extension ParentViewController: ChildViewControllerDelegate { func childViewControllerWillDismiss(childViewController: ChildViewController) { name = childViewController.name } } |
You create the ChildViewControllerDelegate
protocol, set a delegate in the ChildViewController
, make ParentViewController
conforms to this new protocol, and implements the childViewControllerWillDismiss(childViewController:)
delegate method in which you do the actual data passing.
Although it works, the delegation pattern is not really meant to pass data back. For an one-off use case, it’s fine. But what if ParentViewController
can present six different view controllers, and each of these six view controllers want to pass data back? You will have to define six different protocols:
Child1ViewControllerDelegate
Child2ViewControllerDelegate
Child3ViewControllerDelegate
Child4ViewControllerDelegate
Child5ViewControllerDelegate
Child6ViewControllerDelegate
ParentViewController
needs to conform to these six protocols to receive data back, among other more legitimate use of the delegation pattern to communicate back to the parent such as when a delegated task finishes. These six protocols may have legitimate delegation tasks to perform. It would be confusing for them to both perform a delegated task and pass data back. They are doing double duties.
You can certainly separate these two functions into two protocols:
Child1ViewControllerTaskDelegate
andChild1ViewControllerDataDelegate
Child2ViewControllerTaskDelegate
andChild2ViewControllerDataDelegate
Child3ViewControllerTaskDelegate
andChild3ViewControllerDataDelegate
Child4ViewControllerTaskDelegate
andChild4ViewControllerDataDelegate
Child5ViewControllerTaskDelegate
andChild5ViewControllerDataDelegate
Child6ViewControllerTaskDelegate
andChild6ViewControllerDataDelegate
But this means ParentViewController
may potentially need to conform to twelve protocols instead of just six! And if you have other view controllers that need to receive data back during dismissal, you’ll need to consider this kind of complexity and verbosity again.
The new way – Data Store
The Clean Swift architecture provides a dedicated router to handle data passing. The router separates the routing process into two distinct phases:
Routing = Data Passing + Navigation
Navigation
The navigation step is exactly the same as what you normally do, by calling one of the several methods of the UIViewController
class:
show(_:sender:)
showDetailViewController(_:sender:)
present(_:animated:completion:)
dismiss(animated:completion:)
Or, of the UINavigationController
class:
pushViewController(_:animated:)
popViewController(animated:)
popToViewController(_:animated:)
popToRootViewController(animated:)
You may even have to do absolutely nothing. If you use storyboard segues, the presentation and dismissal are automatically handled for you by iOS. The prepare(for:sender:)
method of the UIViewController
is called as usual.
Pass data
Let’s get into the more interesting part. The Clean Swift router implements the routing process (data passing + navigation) in the following three methods:
routeToNextScene(segue:)
passDataToNextScene(source:destination:)
navigateToNextScene(source:destination:)
These methods are named with intuitive prefixes – routeTo, passDataTo, and navigateTo, and parameter names – source and destination. The first method, routeToNextScene(segue:)
invokes the other two methods. Let’s look at an example.
First, let’s look at how we pass data forward from the Parent
to the Child
. In the parent router:
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 |
func routeToChild(segue: UIStoryboardSegue?) { // Get the destination view controller and data store let storyboard = UIStoryboard(name: "Main", bundle: nil) let destinationVC = storyboard.instantiateViewController(withIdentifier: "ChildViewController") as! ChildViewController var destinationDS = destinationVC.router!.dataStore! // Pass data to the destination data store passDataToChild(source: dataStore!, destination: &destinationDS) // Navigate to the destination view controller navigateToChild(source: viewController!, destination: destinationVC) } func passDataToChild(source: ParentDataStore, destination: inout ChildDataStore) { // Pass data forward destination.name = source.name } func navigateToChild(source: ParentViewController, destination: ChildViewController) { // Navigate forward (presenting) source.show(destination, sender: nil) } |
When the forward route from Parent
to Child
starts, Clean Swift invokes the routeToChild(segue:)
method, which does the following three things:
- Get the destination view controller and data store:
- Instantiate the destination view controller either from the storyboard or programmatically
- The destination data store is always available at
destinationVC.router!.dataStore!
, as set up by Clean Swift
- Pass data to the destination data store by calling the
passDataToChild(source:destination:)
method which:- Assigns any data that needs to be passed from the source data store to the destination data store
- Navigate to the destination view controller by calling the
navigateToChild(source:destination:)
method which:- Uses the familiar presenting and dismissing methods in
UIViewController
- Uses the familiar presenting and dismissing methods in
UPDATE Many of you have pointed out the error in the following code in emails and comments. And you’re right. When passing data backward, the router shouldn’t create a new instance of the previous view controller. Instead, it should retrieve the existing view controller reference and assign it to destinationVC
. I felt victim to the copy-and-paste fever. I’m keeping the original here for reference. You’ll find the fixed version below the original. I also created a sample project on GitHub to illustrate how to pass data forward and backward.
Old text starts here
Next, let’s look at how we pass data backward from the Child
to the Parent
. In the child router:
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 |
func routeToParent(segue: UIStoryboardSegue?) { // Get the destination view controller and data store let storyboard = UIStoryboard(name: "Main", bundle: nil) let destinationVC = storyboard.instantiateViewController(withIdentifier: "ParentViewController") as! ParentViewController var destinationDS = destinationVC.router!.dataStore! // Pass data to the destination data store passDataToParent(source: dataStore!, destination: &destinationDS) // Navigate to the destination view controller navigateToParent(source: viewController!, destination: destinationVC) } func passDataToParent(source: ChildDataStore, destination: inout ParentDataStore) { // Pass data backward destination.name = source.name } func navigateToParent(source: ChildViewController, destination: ParentViewController) { // Navigate backward (dismissing) source.dismiss(animated: true, completion: nil) } |
When the backward route from Child
to Parent
starts, Clean Swift invokes the routeToParent(segue:)
method, which does the following three things:
- Get the destination view controller and data store:
- Instantiate the destination view controller either from the storyboard or programmatically
- The destination data store is always available at
destinationVC.router!.dataStore!
, as set up by Clean Swift
- Pass data to the destination data store by calling the
passDataToParent(source:destination:)
method which:- Assigns any data that needs to be passed from the source data store to the destination data store
- Navigate to the destination view controller by calling the
navigateToParent(source:destination:)
method which:- Uses the familiar presenting and dismissing methods in
UIViewController
- Uses the familiar presenting and dismissing methods in
Old text stops here
Next, let’s look at how we pass data backward from the Child
to the Parent
. In the child router:
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 |
func routeToParent(segue: UIStoryboardSegue?) { // Get the destination view controller and data store let destinationVC = viewController?.presentingViewController as! ParentViewController var destinationDS = destinationVC.router!.dataStore! // Pass data to the destination data store passDataToParent(source: dataStore!, destination: &destinationDS) // Navigate to the destination view controller navigateToParent(source: viewController!, destination: destinationVC) } func passDataToParent(source: ChildDataStore, destination: inout ParentDataStore) { // Pass data backward destination.name = source.name } func navigateToParent(source: ChildViewController, destination: ParentViewController) { // Navigate backward (dismissing) source.dismiss(animated: true, completion: nil) } |
When the backward route from Child
to Parent
starts, Clean Swift invokes the routeToParent(segue:)
method, which does the following three things:
- Get the destination view controller and data store:
- Retrieve the existing destination view controller reference
- The destination data store is always available at
destinationVC.router!.dataStore!
, as set up by Clean Swift
- Pass data to the destination data store by calling the
passDataToParent(source:destination:)
method which:- Assigns any data that needs to be passed from the source data store to the destination data store
- Navigate to the destination view controller by calling the
navigateToParent(source:destination:)
method which:- Uses the familiar presenting and dismissing methods in
UIViewController
- Uses the familiar presenting and dismissing methods in
Looks familiar? That’s right. In Clean Swift routing, the direction – forward or backward – doesn’t matter. Passing data is exactly the same regardless of direction. If you need to pass name
, you simply assigns it from the source
to the destination
.
You don’t need to create a ChildViewControllerDelegate
protocol, have ParentViewController
conform to it, and implement the delegate methods. Using the traditional delegation pattern, the more routes you have and the more data you need to pass, the more protocols and delegate methods you’ll need. However, in Clean Swift, the actual navigation and data passing steps are super simple one-liners. Most of the routing details are abstracted to the templates, which you can download for free by subscribing in the form below.
UPDATE Mike Post asked a very good question in the comments below. He wonders why I broke up the prepare(for:sender:)
method into three different methods in the router. Am I just trying to make cool-looking function names. I’m going to explain exactly why below.
In any iOS app, when you need to present another scene, one or two things must happen: (1) navigating to the new scene, and/or (2) pass some data to that new scene. Yes, you can do both in the prepare(for:sender:)
method. In fact, in the simplest case in which you don’t have any data to pass and it’s a simple push, you don’t need to do anything. You don’t even need to implement the prepare(for:sender:)
method because you don’t need to capture the segue identifier to pass data. Hey, you don’t even need to specify a segue identifier in the storyboard! If you’re presenting modally, it’s almost equally simple with a pair of present(_:animated:completion:)
and dismiss(animated:completion:)
calls.
However, imagine you didn’t write this routing code, or if you look at it after 6 months, you’ll have to figure out all the possible routes in the prepare(for:sender:)
method and understand what they’re doing. Without the data store facility for the passed data, you have to be extremely careful not to introduce dependence between all your routes. If you’ve worked on a large app before, you know what I’m talking about.
Therefore, Clean Swift breaks this routing process into two phases explicitly:
Routing = Data Passing + Navigation
The three methods in the router provides much better encapsulation:
- The boring boilerplate of getting the view controller and data store references are encapsulated in the
routeToNextScene(segue:)
method. - The boring mechanics of navigating to the next scene is encapsulated in the
navigateToNextScene(source:destination:)
method. - The interesting details of passing data to the next scene is encapsulated in the
passDataToNextScene(source:destination:)
method.
Clean Swift separates the interesting part from the boring parts. You can get the boring parts right once, and forget about them. You can then focus on the interesting part – the logic of your app. When your requirements change, you only need to update the passDataToNextScene(source:destination:)
method. The boring parts already work, so you don’t even need to look at it.
This is the essence of the router. In fact, this is also the essence of the VIP cycle in Clean Swift. When requirements change (they will), and bugs are discovered (they will, too), you narrow down the area you have to search. You immediately know which class and method you need to look. This makes code maintenance substantially easier.
You can find a sample project on GitHub to illustrate how to pass data forward and backward using the new data store method described in this post. To learn more about how Clean Swift routing works, you can read these related posts:
Something is illogical in the way you described the segue routing. Why do you create the destanation instance from storyboard each time? The system already done this when you triggered the preformed segue function
Yeah I was confused about this too. This seemed like unnecessary boilerplate code if you’ve already set it up in a storyboard. I guess it’s so he doesn’t have to override the prepareForSegue method? But therefore what is triggering routeToChild? …It might as well be the performSegue -> prepareForSegue flow, it seems exactly the same except you’re just customizing the function names for the fun of it.
The main issue I have though is instantiating the parent from the child via a storyboard. It’s a brand new instance. What about the instance of the parent that is already in memory, already presenting the child! What happened to that? It doesn’t make sense, and now we’re sending data to a brand new instance that doesn’t even know it’s presenting a child? Plus also the child needs to know what kind of class the parent is…delegation and/or closures encapsulate this information away from the child. Ahhh, I don’t know. I’m going to have to blog a response on this, I have more questions and issues.
Hi Mike,
That’s a very good question. I’ve updated the end of the post above to explain the philosophy behind routing in Clean Swift.
I have the same problem: to me it looks like creating a new instance of the parent and also the child should not know the exact type of the parent because it reduces the possibility of reusing let’s say an edit scene in two places.
What I did in such situation was still using delegation but at interactor level since we need to exchange business logic: if scene A presents scene B, then router A sets in datastore B a delegate property to the instance of interactor A in the passDataTo method. In this case, interactor B can directly pass data to the parent via its delegate property which is of course a delegate protocol that can be implemented by multiple interactors.
I have updated the post above to fix the error that you all noticed 🙂 And I also created a sample project on GitHub to illustrate the forward and backward routing process. You can find the link above in the post.
ParentScnene presents ChildScene modally via
parentVC.present(childVC)
.In ChildScene I cannot access
parentVC
becausechildVC.presentingViewController
is the root VC. So how can I pass data back into ParentScene in that case?Hi Elias,
In your
ChildRouter
‘srouteToParent(segue:)
method, you’ll want to get a reference to theParentViewController
one way or the other. The Getting Other Related View Controllers section under theUIViewController
‘s documentation lists severals ways you can do so. Either directly usingpresentingViewController
orparent
. Or indirectly throughnavigationController
,splitViewController
, ortabBarController
.Hi ,
First of all thanks for the great article. I have scenario to handle i.e say i have a controller A which present controller B and controller B presents the controller C , now i need to pass data from C to A .Could you tell which approach should i go?. For now i have achieved it through Notifications.Can you provide more simpler and clean approach.
Hi Sanjeev,
You can use the approach outlined in this post to pass the data backward from C to B to A, or directly from C to A. It depends on what you want to achieve. You just need to list the data you expect to pass in the data store protocols for scenes A, B, and C. And then do the actual passing in C’s router.
Hi Raymond,
If we use this flow what will be happen in the navigation stack?
thanks for the article.
Hi bala,
There’s no side effect on the navigation stack. Routing is navigating plus data passing. Navigating and data passing are separate from each other.
Bringing attention again to something Mihai said above, why would you want to directly reference the ParentViewController type? This seems like an unnecessary dependency that will reduce re-usability?
This is fine for single-navigation-path view controllers, but if I was making a reusable screen that could be called in a few different contexts. or even in a different project? As I understand, clean swift places less emphasis on reusing view controllers, but something about having switch statements deciphering the type feels bad to me. Have I misunderstood how this would function in a larger context?
Hi Greg,
Where is the switch statement? In the router? If so, then it’s not about reusing view controllers. It’s about reusing routers. But reusing routers is even less common than reusing view controllers.
I can’t comment on other implementation because I haven’t seen them. But I just don’t see any benefit to use yet another abstraction just for the sake of abstraction. Seems like premature optimization for nothing to me.
Off topic question:
I tried to pass data back from 3rd VC to 1st VC using protocol and delegate, but no data was able to get back. I use the method pop to rootVC from navigationController. My controller goes back to the 1st VC but there’s no data, anyone knows why?
I must have just skimmed this because I didn’t see where the parent uses the data coming back fro m the child. Could you point out in the code where this is handled?
Hi Raymond
How do you think about the idea that we will use closures to send data back to the router and then the router just pass data back to the view controller?
Something just like this:
class ViewController: UIViewController {
func onTapLoginButton() {
router.routeToLogin { isLogin in
// Do something
}
}
}
class Router {
func routeToLogin(completion: @escaping (Bool) -> Void) {
let login = LoginViewController()
login.completion = completion
viewController.show(login)
}
}
Sorry for the mess.
I still don’t get it. The method routeToParent(segue: UIStoryboardSegue?) doesn’t need/get a segue (since you dismiss it manually later), it needs the viewController instance that magically appears in the line:
viewController?.presentingViewController as! ParentViewController
Which then casts presentingViewController to the ParentViewController. I don’t think that was a protocol right? So you’re now coupling the child to the parent in a hard way? What if the child can have multiple different parents (suppose it is a color picker or something like that)?
In no place during your article did you define dataStore or router.
So while this methodology is insteresting, we aren’t seeing the rest of it and thus it might be robbing peter to pay paul.
How the first view controller notifies that the child vc has been dismissed and should update its data?
There must be a closure or delegation or something!