- When using
UINavigationController
, we typically invokepopViewController
through an action event or simply by swiping back from left to right. This process automatically deallocates thetopViewController
of theUINavigationController
, eliminating the need for manual memory management. - When I attempted this with the RIBs framework, I encountered some issues. The
detachChild
method of the router cannot be called directly because the framework does not capture the pop swipe gesture. This limitation leads to a memory leak bug. - Unfortunately, the RIBs framework's examples do not include any navigation examples or solutions. As a result, some developers typically resort to using the
viewDidAppear
method ofUIViewController
for supporting pop swipe gesture. - However, I believe that such implementations indicate a lack of proper understanding of the method's intended purpose. This approach can increase complexity and lead to unexpected errors.
- Let’s solve the problem with UIViewController’s life cycle.
- Let’s handle all navigation cases.
- Simply call the
popViewController
method. - Support swipe-back gesture.
- Support multiple view controllers deallocation case when either the
UITabbar
button is clicked or the navigation back button’s long click occured.
- Simply call the
- Minimize the implementation requirements to the end users.
-
First, We have to know the basic parts of a RIBs.
- Router: A Router listens to the Interactor and translates its outputs into attaching and detaching child RIBs.
- View(View Controller): Views build and update the UI. Views are designed to be as “dumb” as possible. They just display information.
-
Second, We have to know the life cycle of
UINavigationController
.- When UINavigationController pops the UIViewController-C, the delegate method
navigationController(_:didShow:animated:)
is called, with thedidShow
parameter specifically indicating the UIViewController that was shown.
- When UINavigationController pops the UIViewController-C, the delegate method
-
As a result,
- When the
navigationController(_:didShow:animated:)
is called, it is necessary to locate the RIB family(especially the Router) associated with thedidShow
UIViewController. Subsequently, we should invoke thedetach
method to remove child elements from the memory.
- When the
-
As a code
-
Create a common
UINavigationController
and set it to delegate to itself. We define acachedViewController
property to calldetach
method. -
When the
navigationController(_:didShow:animated:)
is called, we handle it based on whether it confirms to the RIBs framework.public class CommonRIBNavigationController: UINavigationController, UINavigationControllerDelegate { private var cachedViewController: [UIViewController] = [] public override func viewDidLoad() { super.viewDidLoad() self.delegate = self } public override func pushViewController(_ viewController: UIViewController, animated: Bool) { super.pushViewController(viewController, animated: animated) if cachedViewController.contains(viewController) == false { cachedViewController.append(viewController) } } public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard cachedViewController.count > children.count else { return } guard let currentIndex = cachedViewController.firstIndex(of: viewController), cachedViewController.count > currentIndex + 1 else { return } // When the last dismissed view controller is kind of a RIBs and CommonRIBsViewController. guard cachedViewController[currentIndex + 1] is ViewControllable, let current = cachedViewController[currentIndex] as? CommonRIBsViewController else { cachedViewController = Array(cachedViewController[0...currentIndex]) return } current.resourceFlusher?.flushRIBsResources() cachedViewController = Array(cachedViewController[0...currentIndex]) }
-
Create a common
UIViewController
andRouter
(RIBs).- CommonRIBsViewController: In this scenario, it must identify the appropriate router to release the resources of the child RIBs.
- CommonRIBsRouter: Using common router is mandatory to find and release RIBs resources.
public protocol CommonRIBsInteractable: Interactable { var resourceFlusher: CommonRIBsResourceFlush? { get } } public protocol CommonRIBsResourceFlush { func flushRIBsResources() } public class CommonRIBsViewController: UIViewController, ViewControllable { public var resourceFlusher: CommonRIBsResourceFlush? { nil } } public class CommonRIBsRouter<InteractorType, ViewControllerType>: ViewableRouter<InteractorType, ViewControllerType>, CommonRIBsResourceFlush { public var nextScreenRouter: ViewableRouting? /** Easy short-cut push method to use same router. */ @discardableResult public func push(nextRouter: ViewableRouting?, animated: Bool) -> Bool { if nextScreenRouter != nil { flushRIBsResources() } guard let next = nextRouter else { return false } nextScreenRouter = next nextScreenRouter?.interactable.activate() nextScreenRouter?.load() viewControllable.uiviewController.navigationController?.pushViewController(next.viewControllable.uiviewController, animated: animated) return true } /** Deactivate RIBs resource and set nil */ public func flushRIBsResources() { nextScreenRouter?.interactable.deactivate() nextScreenRouter = nil } }
-
Example
final class MasterViewController: CommonRIBsViewController, MasterPresentable { override var resourceFlusher: CommonRIBsResourceFlush? { guard let listener = listener as? CommonRIBsInteractable else { return nil } return listener.resourceFlusher } // ... Do other things... } protocol MasterInteractable: CommonRIBsInteractable { // ... Do other things... } final class MasterInteractor: PresentableInteractor<MasterPresentable>, MasterInteractable { public var resourceFlusher: CommonRIBsResourceFlush? { router as? CommonRIBsResourceFlush } // ... Do other things... } final class MasterRouter: CommonRIBsRouter <MasterInteractable, ViewControllable>, MasterRouting, LaunchRouting { // ... Do other things... func routeToDetail(at case: MasterExampleCase) { let detail = DetailBuilder(dependency: EmptyComponent()).build(`case`) // Use common push method.` push(nextRouter: detail, animated: true) } }
-