Episode #271

Moving Review Logic

Series: Refactoring to Coordinators

17 minutes
Published on May 18, 2017

This video is only available to subscribers. Get access to this video and 572 others.

Moving on to the next segue in our storyboard, this time Ben and Soroush tackle the Add Review flow. They discuss naming of delegates, the ideal place to perform logic such as preparing a model to be saved and where mutations to the model live. They end up with a view controller that is completely decoupled from the AddReviewViewController and a better picture of what the coordinator tends to look like.

Episode Links

A little Cleanup

The AppCoordinator is starting to take shape, so it's time to move it into its own file. We start by creating AppCoordinator.swift and moving the code into it:

// AppCoordinator.swift
import UIKit

class AppCoordinator : RestaurantsViewControllerDelegate     {
    let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let restaurantsVC = navigationController.topViewController as! RestaurantsViewController
        restaurantsVC.delegate = self
    }

    func didSelect(restaurant: Restaurant) {
        let restaurantDetail = RestaurantViewController.makeFromStoryboard()
        restaurantDetail.restaurantDelegate = self
        restaurantDetail.restaurant = restaurant
        navigationController.pushViewController(restaurantDetail, animated: true)
    }

    func addReviewTapped(_ vc: RestaurantViewController) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let nav = storyboard.instantiateViewController(withIdentifier: "AddReviewNavigationController") as! UINavigationController
        let reviewVC = nav.topViewController as! ReviewViewController
        reviewVC.addReviewBlock = { [weak self] rvc in
            let review = Review(author: rvc.nameTextField.text ?? "",
                                comment: rvc.commentTextView.text,
                                rating: Float(rvc.ratingView.value),
                                restaurantID: vc.restaurantID)
            Restaurants.add(review: review)
            self?.navigationController.dismiss(animated: true) {
                vc.insertReview(review)
            }
        }

        navigationController.present(nav, animated: true)
    }
}

This can live right next to the AppDelegate.swift in the project organizer.

Adding our Next Transition

The next transition to handle is the Add Review logic. Currently this is done as a modal transition by the RestaurantViewController.

We'll start by removing the segue in the Storyboard. Then we can add the appropriate delegate protocol so the coordinator can handle the actions triggered by this view controller:

protocol RestaurantViewControllerDelegate : class {
    func addReviewTapped(_ vc: RestaurantViewController)
}

class RestaurantViewController : UITableViewController {
    weak var restaurantDelegate: RestaurantViewControllerDelegate?

    // ... 
}

The naming of the property here is slightly awkward because UITableViewController already has a delegate property. Other names we might choose: userActionDelegate, or perhaps coordinationDelegate.

Defining the Action Explicitly

Using segues we can wire up a transition from a button with no code. Since we don't have segues anymore we need to add an @IBAction for the button tap.

We'll just send this off to our restaurantDelegate to handle it.

@IBAction func addReview(_ sender: Any) {
    restaurantDelegate?.addReviewTapped(self)
}

Comforming to the Protocol

Our AppCoordinator will handle these actions, so it will need to conform to this protocol. We can move the code that was previously in our prepare(for segue: sender:) code.

class AppCoordinator : RestaurantsViewControllerDelegate, RestaurantViewControllerDelegate {
    // ...

    func addReviewTapped(_ vc: RestaurantViewController) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let nav = storyboard.instantiateViewController(withIdentifier: "AddReviewNavigationController") as! UINavigationController
        let reviewVC = nav.topViewController as! ReviewViewController
        reviewVC.addReviewBlock = { [weak self] rvc in
            let review = Review(author: rvc.nameTextField.text ?? "",
                                comment: rvc.commentTextView.text,
                                rating: Float(rvc.ratingView.value),
                                restaurantID: vc.restaurantID)
            Restaurants.add(review: review)
            self?.navigationController.dismiss(animated: true) {
                vc.insertReview(review)
            }
        }

        navigationController.present(nav, animated: true)
    }
}

We also need to wire up this delegate:

func didSelect(restaurant: Restaurant) {
    let restaurantDetail = RestaurantViewController.makeFromStoryboard()
    restaurantDetail.restaurantDelegate = self
    restaurantDetail.restaurant = restaurant
    navigationController.pushViewController(restaurantDetail, animated: true)
}

With this in place we can remove our segue handling code, as this is already implemented in our coordinator.

Reviewing What We've Done

If you look at the RestaurantViewController now, it is still aware of reviews and the review model, but has no reference whatsoever to the AddReviewViewController. Reducing dependencies in this way can clean up code and make your code more flexible and amenable to change.

This episode uses Ios 10.3, Xcode 8.3.