Skip to content

wlsdms0122/RVB

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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> { _completed.asObservable }
    
    private var cancellableBag = Set<AnyCancellable>()

    // MARK: - Initiailzer
    override init(rootView: View) {
        super.init(rootView: view)
        
        // Event adapting
        rootView.completed
            .sink { [weak self] in self?._completed.accept($0) }
            .store(in: &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.

Routing

In RVB, again emphasize that module is view. Therefore "routing" means view presenting.

For example there is scenario that A route to B, B send event to parent and A route to C.

class AViewController: AControllable {
    ...
    func presentB() {
        guard let viewController = router?.routeToB(with: .init()) else { return }
        
        viewController.event
            .subscribe(onNext: { [weak self] in self?.presentC() })
            .disposed(by: viewController.disposeBag)
        
        navigationController?.popToViewController(self, animated: true) // ⚠️
        navigationController?.pushViewController(viewController, animated: true)
    }
    
    func presentC() {
        guard let viewController = router?.routeToC(with: .init()) else { return }
        
        navigationController?.popToViewController(self, animated: true) // ⚠️
        navigationController?.pushViewController(viewController, animated: true)
    }
}

The important thing is that View should guarantee presenting state about routing.

Because the other architectures manage modules own ways like Router's attach, detach of RIBs. but RVB's modules are depend on UI system.

View know that how to present own state exactly when child module(view) attached. and in example, when C module presenting, A know that B should be disappear.

Deeplink

You can implement deeplink flow through the Routing & Communication section.

First, you can start by referencing the Deeplinker & Route packages to assist with your deeplink implementation.

It is crucial that each module takes full responsibility for routing. With this approach, the deeplink handler becomes a straightforward sequence of routing requests, instead of a complex process in and of itself. The deeplink handler simply arranges the order of each module.

Let's explain what is mean "each module takes full responsibility for routing" with example.

protocol ParentControllable {
    ...
    func presentChild(animated: Bool, force: Bool, completion: ((ChildControllable) -> Void)?)
}
func presentChild(
    animated: Bool, 
    force: Bool,
    completion: ((ChildControllable) -> Void)?
) {
    // #1
    // `search(where:)` search view controllers that satisfied where clause.
    if let child = search(where: { $0 is ChildControllable })
        .last as? ChildControllable {
        // #2
        guard force else {
            completion?(child)
            return
        }
        // `route(_:animated:completion:)` is route to view controller if possible.
        route(child, animated: animated) {
            completion?(child)
        }
        return
    }

    // #3
    let child = router?.routeToChild(with: .init())

    child.completed
        .subscribe(onNext: { [weak self] in
            guard let self else { return }
            self.route(self, animated: true) { _ in }
        })
        .disposed(by: disposeBag)

    // #4
    route(self, animated: animated) {
        $0?.navigationController.pushViewController(child, animated: animated)
        completion?(child)
    }
}

#1. The routing method should check if the destination already exists. When searching for a child module, caution should be taken to search the farthest one.

The relationship between a parent and child module is normally not relevant and is only meant to facilitate the flow.

But in some cases, the relation is important and needs to be guaranteed for the flow. For example, there is a deeplink to route to the detail of a product in a commerce app. If the user has already entered the detail with ID 1, the deeplink's destination should be the detail with ID 2. However, the code snippet mentioned above would not be enough, as the search clause does not provide sufficient differentiation.

To resolve this issue, additional conditions can be added to differentiate between modules.

func presentDetail(
    animated: Bool,
    force: Bool,
    id: String, 
    completion: ((DetailControllable) -> Void)?
) {
    if let child = search(where: { 
        guard let child = $0 as? ChildControllable else { return false }
        return child.id == id
    })
        .last as? ChildControllable {
        ...
    }
    ...
}

Or, if you do not want to check existence every time you instantiate a module, you can avoid writing a search clause.

#2. The force parameter is used to force the routing even if the destination will not be the top most view controller. This can occur when the deeplink is searching for the child and is already exist in stack. Using the force parameter will ensure that the routing takes place.

#3. If child not already exists, instantiate new module and handle events.

When a child event triggers another routing event, such as dismissing the child, the parent should consider its own routing state, as described in the Routing section.

#4. New child module routing should start from its parent. So routing logic guarantee route back to parent.

func routeToChildLink() {
    presentParent(animated: true, force: false) { parent in
        parent.presentChild(animated: true, force: true)
    }
}

The deeplink handler, typically the root controller or another designated component, handles the deeplink when it receives a handle request.

The deeplink handler first performs its own routing, and in the completion handler, it uses controllable from closure parameter to route to the next module.

func routeToProfileLink() {
    presentSignedOut(
        animated: true,
        force: false,
        signedIn: { main in
            main.presentProfile(animated: true, force: true)
        },
        completion: nil
    )
}

If you need to wait until user interaction is complete, such as signing in, then you should structure the routing logic to deliver results.

It can be delegate & closure or any streams(Observable, Publisher).

Let me explain in more detail. You may already have structured a sequence where the root presents the signed out view, the signed out view performs signing in, and then the root presents the main view after receiving the signed in event from the signed out view.

When the signing in process is complete, the root module routes to the main automatically, as the inter-module communication logic has already taken care of that.

Thus the presentSignedOut(animated:force:signedIn:completion:) method should deliver the completion of the main routing through the signedIn parameter.

Via description, The structure of the deeplink is simply an arrangement of modules, each of which should be responsible for processing its own responsibilities and providing public interfaces.

Sample

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", exact: "1.1.0")
]

License

RVB is available under the MIT license.