Bringing RIB architecture to SwiftUI
This is an adaptation of RIB architecture to make it work with SwiftUI. To learn more about the fundamentals of RIB, please take a look at Uber's official documentation.
Even though I like the core ideas of RIB, there are a few things in the original RIB (for UIKit) that I don't like. For example, it adds lifecycle to some of the elements, which creates blackbox code that we have to read more code to find out when a callback is triggered, hence, introducing some learning curve. Or the use of Component as a dependency container. It does help reduce some boilerplate code, but on the flip side, it creates ambiguity and some occasional naming collisions. I like to make things as explicit as possible.
To solve those things. Here are the changes:
- No more super classes. No more lifecycle. If you want to hook up some event with the view lifecycle, do it manually, it doesn't take much time and now a new developer who has never used RIB before only need to look at 2 files to find out when something is triggered.
- No
Component, dependency container is a simple struct that contains all the dependencies. You collect all the dependencies, put them in that struct and give it to the builder, no more meddling with naming, type matching of 1 bigComponent. - Parent of UIKit RIB communicates with the RIB using streams (Parent -> RIB) and
Listenerdelegate (RIB -> Parent). Because SwiftUI is set up to work nicely withCombine, I decided to remove the delegate and use streams for communication in both directions. With this change, you'll have to dispose the subscription at the time of RIB detaching. - Internal interactions within UIKit RIB is also done with delegates. That is gone as well,
StateContainerandViewContainerare introduced to replace the delegates.
What each component does and the interactions among them are described as the following:
- (1)
Viewevents (appear, tap, scrolling, gestures, etc.) are sent to theInteractorusing theInteractorprotocol's methods. - (2)
Interactorprocesses logic, it then updates the state if needed.StateContaineris where the state is stored.StateContaineris anObservableObjectand its properties are wrapped with@Publishedso thatViewcan listen to those. - (3) If there is something happens that this RIB needs to inform its parent, the
Interactorwill publish the events throughPublisherContainer.PublisherContainercontainsAnyPublishers. You can either createSubjects in theInteractorand expose the publisher, or if possible, you can even reuse properties ofStateContainerthanks to the little trick of adding$in front of the property name to get itsPublisher. Like sostateContainer.$isLoading. - (4)
Interactorcan also callRouterto ask it to attach or detach other RIBs. After attaching the new RIB,Routerwill return theOutputof that RIB to theInteractor, theInteractorcan now use thePublisherContainerinside theOutputto listen to the newly attached RIB and handle as required. - (5)
Routerupdates theViewContainerwhen attach or detach is called.ViewContaineris similar toStateContainerexcept that it contains the children'sViews and some other View-related properties if applicable. - (6) (7) Because
StateContainerandViewContainerareObservableObjects, changes to these 2 will be propagated toViewso that the UI can be updated. - (8) Events from this RIB are propagated to its parent via the publishers of
PublisherContainer.
A few notes here:
- View holds the reference to
Interactor,StateContainer, andViewContainer;Interactorholds the reference toRouterandStateContainer;Routerholds the reference toViewContainer. Because of this,Viewis the holder of everything; when theViewis deallocated, the entire RIB will be deallocated. This is different from UIKit RIB, whereRouteris the holder. - All relationships are one-way, therefore, there is no need for weak references.
- When detaching a RIB, remember to remove the view references in
ViewContainerand dispose the subscriptions of streams inInteractor. - Properties of
ViewContainerandStateContainershould befileprivate(set)to ensure that other components cannot modify them. This is to ensure the rigidity of the RIB.
In this repo, you can find a template for Xcode new file. This is to help you create a new RIB quickly. The template for writing tests is to be added later.
You can find a small demo project in this repo as well