Pass data backward more elegantly without using delegation

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:

  1. Delegation
  2. 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:

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 and Child1ViewControllerDataDelegate
  • Child2ViewControllerTaskDelegate and Child2ViewControllerDataDelegate
  • Child3ViewControllerTaskDelegate and Child3ViewControllerDataDelegate
  • Child4ViewControllerTaskDelegate and Child4ViewControllerDataDelegate
  • Child5ViewControllerTaskDelegate and Child5ViewControllerDataDelegate
  • Child6ViewControllerTaskDelegate and Child6ViewControllerDataDelegate

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:

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

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:

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

Old text stops here


Next, let’s look at how we pass data backward from the Child to the Parent. In the child router:

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

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:

Raymond
I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

20 Comments

  1. 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

    1. 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.

      1. 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.

  2. 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.

  3. 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.

  4. ParentScnene presents ChildScene modally via parentVC.present(childVC).
    In ChildScene I cannot access parentVC because childVC.presentingViewController is the root VC. So how can I pass data back into ParentScene in that case?

    1. Hi Elias,

      In your ChildRouter‘s routeToParent(segue:) method, you’ll want to get a reference to the ParentViewController one way or the other. The Getting Other Related View Controllers section under the UIViewController‘s documentation lists severals ways you can do so. Either directly using presentingViewController or parent. Or indirectly through navigationController, splitViewController, or tabBarController.

  5. 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.

    1. 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.

        1. 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.

  6. 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?

    1. 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.

  7. 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?

  8. 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?

  9. 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)
    }
    }

  10. 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)?

  11. 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.

  12. 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!

Leave a Comment

Your email address will not be published. Required fields are marked *