The name RVB is short for Router, View, Builder.
It get inspired by architectures like VIPER, RIBs. Extracted concept of module manage & intercommunicate from these architectures.
RVB focus on problems that occur between modules.
Module is series of objects composed by view in RVB.(Any modules in domain layer not guide directly in RVB, but you can compose dependencies through Builder.) Thus the module depend on view's lifecycle.
When the view allocated, associated objects in module instantiate or retained.
Due to this feature, View can use any view base architectures like MVC, MVP, MVVM or ReactorKit.
Router manage child route paths. It retain child module's Buildable
s.
Normally router doesn't perform special build process, but child module conversion may be required to fit the current module's view system due to each module is consist independently.
For example, if parent & child module systems are different like that UIKit
& SwiftUI
or RxSwift
& Combine
,
you may need to convert the child module to fit the current module.
public protocol ChildControllable: ViewControllable {
/// Child sequence completed event.
/// It implemented with `Combine`.
var completed: AnyPublisher<Void, Never> { get }
}
/// Redefine child module protocol to fit the current module.
protocol ChildControllable: UIViewControllable {
var disposeBag: DisposeBag { get }
/// Implemented with `RxSwift`
var completed: Observable<Void> { get }
}
/// Adapter class to convert module system.
final class ChildControllableAdapter<View: Child.ChildControllable>: UIHostingController<View>, ChildControllable {
// MARK: - Property
private let _completed = PublishRelay<Void>()
var completed: Observable<Void> { _done.asObservable }
private var cancellableBag = Set<AnyCancellable>()
// MARK: - Initiailzer
init(view: View) {
super.init(rootView: view)
// Event adapting
view.completed
.sink { [weak self] in self?._completed.accept($0) }
.store(&cancellableBag)
}
}
protocol ParentRoutable: Routable {
func routeToChild(with parameter: ChildParameter) -> ChildControllable
}
final class ParentRouter: ParentRoutable {
...
func routeToChild(with parameter: ChildParameter) -> ChildControllable {
let controllable = childBuilder.build(with: paramter)
return ChildControllableAdapter(view: controllable)
}
}
Builder is responsible for all about modules instanting.
The module can only use builders to instantiate other completely assembled modules.
Module is series of objects composed by view as mentioned above.
Builder instantiate module with instantiate or injected dependencies. and return Controllable
that communication protocol between modules.
Router manage child route path using child builders. View request route to Router, it return Controllable
for module communication(View).
public struct ProductDetailDependency {
// MARK: - Property
let productService: ProductServiceable
// MARK: - Initializer
public init(productService: ProductServiceable) { ... }
}
public struct ProductDetailParameter {
// MARK: - Property
let id: String
// MARK: - Initializer
public init(id: String) { ... }
}
public protocol ProductDetailBuildable: Buildable {
func build(with parameter: ProductDetailParameter) -> ProductDetailControllable
}
public final class ProductDetailBuilder: Builder<ProductDetailDependency>, ProductDetailBuildable {
public func build(with parameter: ProductDetailParameter) -> ProductDetailControllable {
...
}
}
Builder's components have Dependencies and Parameters.
Dependency is static dependencies for module(like domain layer objects). dependencies that passed through Dependency are managed in Builder scope.
It mean Dependency graph is affected by Builder graph.
Parameter is dynamic dependencies for module(like selected product id). Mostly data generated by the current module.
Dependency injection is performed in build(with:)
using Dependency & Parameter.
public final class ProductDetailBuilder: Builder<ProductDetailDependency>, ProductDetailBuildable {
public func build(with parameter: ProductDetailParameter) -> ProductDetailControllable {
let viewController = ProductDetailViewController()
let reactor = ProductDetailViewReactor(
productService: dependency.productService,
id: parameter.id
)
let router = ProductDetailRouter()
viewController.router = router
viewController.reactor = reactor
return viewController
}
}
In this example, dependency(ProductService
) is injected from parent, but you can instantiate dependency in own module.
And then this case instantiated dependency's top of object graph is Builder.(Object alive until Builder deallocated.)
The module can communicate only parent <-> child directly using Controllable
protocol.
Each module should define Controllable
protocol for communication. It contain send/receive events.
public protocol ChildContraollable: ViewControllable {
var completed: AnyPublisher<Void, Never> { get }
}
In this case, child module send completed event that sequence completed.
The protocol's form is free. You can define delegate pattern
, Observable
(Rx), Publisher
(Combine) for send events, and function
, Subject
for receive events.
final class ParentViewController: UIViewController, ParentControllable {
...
func presentChild() {
let controllable = router?.routeToChild(.init())
// Receive event.
controllable.completed
.sink {
// Do something.
}
.store(&cancellableBag)
present(controllable, animated: true)
}
...
}
The parent module can receive event through controllable protocol.
Sometimes module should communicate with far module not direct child.
For example, in sign up flow, Root
module wait for registration event about A
(welcome) -> B
(terms) -> C
(input basic information) -> D
(congratulation).
In RVB, the Root
module should listen event from the module A
and not the module D
.
public protocol AControllable: UIViewControllable {
var completed: AnyPublisher<Void, Never> { get }
}
You can define completed event to all modules, but not recommended.
Bypass event stream through Builder
& Dependency
. (A -> B, C, D)
public struct BDependency {
let completed: AnyPublisher<Void, Never>
public init(completed: AnyPublisher<Void, Never>) {
self.completed = completed
}
}
...
public final class BBuilder: Builder<BDependency>, BBuildable {
public func build(with parameter: BParameter) -> BControllable {
let cBuilder = CBuilder(.init(completed: dependency.completed))
...
}
}
And inject shared object into the module if needed.
public final class DBuilder: Builder<DDependency>, DBuildable {
public func build(with parameter: DParameter) -> DControllable {
...
let viewModel = DViewModel(completed: dependency.completed)
...
}
}
When module D
job completed, emit event via injected stream. Then you can skip the layers and listen for events.
Package is served, but it's not required. It contain some protocols for namespace, convenience. But it's very simple and less code.
Ultimately, RVB doesn't want to create constraints on development. It just guide.
dependencies: [
.package(url: "https://github.com/wlsdms0122/RVB.git", branch: "main")
]
To be continue...
RVB is available under the MIT license.