Handling navigation with UINavigationController

Heyo,

I feel like the only one using TCA with UIKit, but perhaps there are others out there. And if there are they've probably also run across the issue of handling popping view controllers in a navigation controller. Presumably the pushed view controller is tied to some optional state somewhere and whenever the view controller is popped you want to nil that out.

I wrote this UINavigationController subclass that handles that automatically by attaching an action to a pushed view controller. I don't know if this is a good idea though. So I'm posting here in hopes of getting some code review or other input on the topic!

import UIKit

open class NavigationController<Action>: UINavigationController {

  private var popActions: [UIViewController: Action] = [:]

  public var popActionHandler: ((Action) -> Void)?

  public func pushViewController(_ viewController: UIViewController, animated: Bool, withPopAction popAction: Action) {
    popActions[viewController] = popAction
    super.pushViewController(viewController, animated: true)
  }

  open override func popViewController(animated: Bool) -> UIViewController? {
    let popped = super.popViewController(animated: animated)
    popped.map(performActionAndClear)
    return popped
  }

  open override func popToRootViewController(animated: Bool) -> [UIViewController]? {
    let popped = super.popToRootViewController(animated: animated)
    popped.map(performActionAndClear)
    return popped
  }

  open override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
    let popped = super.popToViewController(viewController, animated: animated)
    popped.map(performActionAndClear)
    return popped
  }

  private func performActionAndClear(for viewController: UIViewController) {
    guard let action = popActions[viewController] else { return }
    popActionHandler?(action)
    popActions.removeValue(forKey: viewController)
  }

  private func performActionAndClear(for viewControllers: [UIViewController]) {
    viewControllers.forEach(performActionAndClear)
  }

}

It's pretty straight forward, keep track of pushed view controllers and their associated action. Whenever we pop one or multiple (I think this should handle the new tap-and-hold-the-back-button navigation in iOS 14 but haven't double checked yet) view controllers we call the popActionHandler with that associated action.

So in practice we would have a an instance of this navigation controller setup like this:

let myNavigationController = NavigationController<MyFeatureAction>(rootViewController: featureRootViewController)
myNavigationController.popActionHandler = { [weak self] action in self?.viewStore.send(action) }

And then whenever we push a view controller we also pass along the associated pop-action:

store
      .scope(state: { $0.recipeCollectionViewState }, action: PlanFeatureAction.recipeCollectionViewAction)
      .ifLet { [weak self] scoped in
        let recipeCollectionViewController = RecipeCollectionView(store: scoped)
        self?.pushViewController(recipeCollectionViewController, animated: true, withPopAction: .didPopRecipeCollectionView)
      } else: { [weak self] in
        guard let self = self else { return }
        self.navigationController?.popToViewController(self, animated: true)
      }
      .store(in: &cancellables)

Thoughts?

Back in time of the creation of this thread I've been using TCA with UIKit a ton and also created a package with a bunch of helpers for it, unfortunately I found this thread just about 2 weeks ago :sweat_smile:

Now both package and thread are outdated but I'm working on a major update with a bunch of cleanup, tests and improved navigation

Navigation is derived into a separate package now and I just pushed a draft of the example to wip navigation-stacks branch, but it's already works nice with both tree and stack navigation patterns, I hope to finish the example, add more tests and release at least CombineNavigation 1.0.0 this year :new_moon_with_face:

Here is my working branch: