/RVB

Primary LanguageSwiftMIT LicenseMIT

RVB

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.

Components

View

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

Router manage child route paths. It retain child module's Buildables.

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

Builder is responsible for all about modules instanting.

The module can only use builders to instantiate other completely assembled modules.

Module

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).

Dependency & Parameter

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

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.)

Communication

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.

Shared Object

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.

Installation

Swift Package Manager

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")
]

Sample

To be continue...

License

RVB is available under the MIT license.