Xcoding with Alfian

Mobile development articles and tutorials

Refactoring iOS app with Coordinator Pattern for Navigation

Alt text

In a typical iOS app that uses MVC as the architecture, the View Controller has to handle the navigation between other View Controllers. It means that the View Controller must know in advance the other controllers that it will navigate to. This creates a tight coupling between the controllers that we should avoid whenever possible.

class MovieListViewController: UICollectionViewController {
  
  //...
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let movieDetailVC = segue.destination as? MovieDetailViewController, let cell = sender as? UICollectionViewCell, let indexPath = collectionView.indexPath(for: cell)  else {
      fatalError("Unexpected View Controller")
     }

     let movie = movies[indexPath.item]
     movieDetailVC.movie = movie
  }
  
}

This approach also add additional responsibility for the View Controller thus violating the single responsibility principle (SRP).

Enter the Coordinator Pattern

In recent years, there is a new pattern that emerges to solve this navigation problem, it’s called the Coordinator pattern. Here are the breakdown of how it works:

  1. The Coordinator main responsibility is to handle all the logic for presentation between View Controllers.
  2. The View Controller uses delegation to communicate back to the Coordinator when it wants to present other view controller or navigating back.
  3. The Coordinator accepts a presenting View Controller (usually Navigation Controller), then setup the View Controller with all the required properties to navigate, and perform the actual navigation.
  4. The app has one main application coordinator and every View Controller will have their own coordinator class.

That’s the overview of the Coordinator, let’s move to the next section where we will refactor an app that uses standard storyboard segue to navigate between view controller.

What we will refactor

Alt text

The app has 2 separate screens, one is the MovieList screen and the other one is MovieDetail screen. Whenever user taps on the MovieCell in the list, it will perform the segue to the MovieDetail screen passing the Movie in the selected index. Here are the things that we will do to refact the app navigation into Coordinator pattern:

  1. Create a Coordinator protocol, then create concrete coordinator classes for application , movie list vc , and movie detail vc screens.
  2. Initialize our app programatically, set the initial coordinator, and start the first navigation from AppDelegate .
  3. Create delegate in MovieListViewController that will be used to communicate back to MovieListCoordinator to setup and navigate to MovieDetailViewController.

To begin, let’s clone the starter project using the GitHub repository below. It contains all the View Controllers, model, the cells for Table View and Collection View. alfianlosari/MovieCoordinator-Starter-Project. Run pod install , then try to build and run the app, then play with it a bit.

Defining Coordinator Protocol

To begin, let’s create a new Protocol with the name of Coordinator , this protocol only has one function start which will be invoked to setup and perform the navigation in concrete class.

protocol Coordinator {
     
    func start()
}

Building MovieDetailCoordinator

Next, let’s create the coordinator for the MovieDetailViewController , which is the MovieDetailCoordinator. Here are the details of the implementation:

  1. MovieDetailCoordinator has an initializer that accepts the presenter view controller and the movie object, then it stores the properties as an instance properties.
  2. MovieDetailCoordinator implements the Coordinator protocol and provide the implementation of start , which is to setup the MovieDetailViewController with a movie and using the presenter navigation controller to push the MovieDetailViewController to it’s navigation stack.
import UIKit

class MovieDetailCoordinator: Coordinator {
    
    private let presenter: UINavigationController
    private var movieDetailViewController: MovieDetailViewController?
    private let movie: Movie
    
    init(presenter: UINavigationController, movie: Movie) {
        self.presenter = presenter
        self.movie = movie
    }
    
    func start() {
        let movieDetailViewController = MovieDetailViewController()
        movieDetailViewController.movie = movie
        self.movieDetailViewController = movieDetailViewController
        
        presenter.pushViewController(movieDetailViewController, animated: true) 
    }
}

Building MovieListCoordinator

Next, let’s move to the MovieListViewController . First, we have to set how the MovieListViewController will communicate with the coordinator. We are going to use a delegate for this. So declare the MovieListViewControllerDelegate like so.

protocol MovieListViewControllerDelegate: class {
    
    func movieListViewController(_ controller: MovieListViewController, didSelect movie: Movie)
    
}

Navigate to the declaration of MovieListViewController to add a new weak variable with the type of MovieListViewControllerDelegate . Also delete the prepareForSegue method and update the collectionView(_didSelectItemAtIndexPath:) method like the code below to invoke the delegate and pass the user selected movie.

class MovieListViewController: UICollectionViewController {
    
    weak var delegate: MovieListViewControllerDelegate?
    //.....
  
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let movie = movies[indexPath.item]
        delegate?.movieListViewController(self, didSelect: movie)
    }
  
}

Next, let’s create the MovieListCoordinator class for the MovieListViewController . Here are the breakdown of how we will implement the Coordinator pattern for this class:

  1. MovieListCoordinator accepts the navigation controller and array of movies as for the initializer parameter. It stores those as instance properties.
  2. It also has an optional property for the MovieListViewController that will be set in start method and the MovieDetailCoordinator in the implementation of MovieListViewControllerDelegate for the coordinator.
  3. In start method, the MovieListViewController is initialized, then the movies array is assigned to the VC. At last, the presenter navigation controller will push the VC to its navigation stack.
  4. In the movieListViewController(didSelectMovie:) method, the MovieDetailCoordinator is initialized passing the selected Movie instance. At last, the coordinator’s start method is invoked to navigate to MovieDetailViewController.
import UIKit

class MovieListCoordinator: Coordinator {
    
    private var presenter: UINavigationController
    private var movieDetailCoordinator: MovieDetailCoordinator?
    private var movieListViewController: MovieListViewController?
    private var movies: [Movie]
    
    init(presenter: UINavigationController, movies: [Movie]) {
        self.presenter = presenter
        self.movies = movies
    }
    
    func start() {
        let movieListViewController = MovieListViewController()
        movieListViewController.movies = movies
        movieListViewController.delegate = self
        
        self.movieListViewController = movieListViewController
        presenter.pushViewController(movieListViewController, animated: true)
    }
    
}

extension MovieListCoordinator: MovieListViewControllerDelegate {
    
    func movieListViewController(_ controller: MovieListViewController, didSelect movie: Movie) {
        let movieDetailCoordinator = MovieDetailCoordinator(presenter: presenter, movie: movie)
        
        self.movieDetailCoordinator = movieDetailCoordinator
        movieDetailCoordinator.start()
    }
    
}

Integrating with ApplicationCoordinator & AppDelegate

The final part of the puzzle is building the ApplicationCoordinator . Here are the breakdown of the coordinator implementation:

  1. This will be the root/parent coordinator for our application, so it will accept UIWindow , initialize root view controller withUINavigationController , and instantiate the MovieListCoordinator .
  2. In the start method, it will set the window’s root view controller with the navigation controller, and start the MovieListCoordinator to push the MovieListViewController as the first screen.
import UIKit

class ApplicationCoordinator: Coordinator {
    
    private let window: UIWindow
    private let rootViewController: UINavigationController
    private var movieListCoordinator: MovieListCoordinator?
    
    init(window: UIWindow, movies: [Movie]) {
        self.window = window
        rootViewController = UINavigationController()
        rootViewController.navigationBar.prefersLargeTitles = true
        
        movieListCoordinator = MovieListCoordinator(presenter: rootViewController, movies: movies)
    }
    
    func start() {
        window.rootViewController = rootViewController
        movieListCoordinator?.start()
        window.makeKeyAndVisible()
    }
    
}

At last, we need to delete main.storyboard file for the project, as we will be instantiating our app window programatically. Make sure to also delete main interface name in General tab in the project.

Alt tex

In AppDelegate , we need to perform several configurations:

  1. Initialize a UIWindow instance using our screen bounds .
  2. Initialize ApplicationCoordinator passing the window and array of movies .
  3. Store the window and application coordinator in instance properties.
  4. Start the ApplicationCoordinator.
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var applicationCoordinator: ApplicationCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let window = UIWindow(frame: UIScreen.main.bounds)
        let appCordinator = ApplicationCoordinator(window: window, movies: Movie.dummyMovies)
        self.window = window
        self.applicationCoordinator = appCordinator
        
        appCordinator.start()
        
        return true
    }

}

That’s it try to build and run the project to see the Coordinator pattern in action! You can clone the completed project GitHub repository at alfialosari/MovieCoordinator-Completed.

Conclusion

That’s it!, we have successfully decouple all the navigation logic inside the View Controllers using the Coordinator pattern. It reduces responsibility of the View Controller for navigation to a separate coordinator object. Using this pattern helps us to write a much better encapsulated and separate abstraction of responsibilities between classes. Let’s keep the lifelong learning goes on and keep on building with Swift 😋😋😋.