uber / RIBs

Uber's cross-platform mobile architecture framework.
https://eng.uber.com/tag/ribs/
Apache License 2.0
7.75k stars 905 forks source link

[iOS] Dependency(?) injection after Router has been created. #303

Open JulianHunt opened 5 years ago

JulianHunt commented 5 years ago

From the tutorials it seems that the dependencies must be passed in to a child rib before you are ready to display said child Rib. What is the convention for passing data that will be needed by the child rib that is entered into (i.e: a textfield) the parent rib?

For example: -Rib A has a spinner picker that lets you pick one of 3 types (an address, a contact or a credit card) and a button that triggers Rib B. -Rib B displays a view to add an object of the type selected in Rib A. (Rib A is a table view with a cell that contains a text field for each component of the object type selected in Rib A) -How should that type be passed from Rib A to Rib B? The builder for Rib B which takes in the dependency/component is created when Rib A is created/displayed as this is done when Rib A's router is created. This is done before the user has selected a Type. Note: Trying to find a solution without Rx as our dev team is mainly comprised of junior developers and converting the project to Rx would add a lot of complexity that I don't think our team is ready to handle yet.

My current solution is to still create the builder for Rib B when Rib A's router is created however I pass the needed type through the protocol methods to Rib A's router and modified the .build() function to also take in that type which can then be passed to Rib B's interact and used to setup Rib B. However this feels like I'm passing in dependencies in two spots and doesn't seem like the ideal design, although it does work. This will become ugly if there are multiple values that need to be passed between views and would end up creating another "dependency" type object to pass to the child Rib.

ermik commented 5 years ago

Use RxSwift, ability to mutate reference types, or rebuild the rib using the builder preserved in the parent router.

JulianHunt commented 5 years ago

Trying to find a solution without Rx, team is to junior for adding Rx to be viable right now.

ermik commented 5 years ago

@JulianHunt you case is perfect for Rx. As far as I can see you have to sibling RIBs that need to communicate and this architecture (I assume you've made a conscious choice to use it) makes heavy use of Rx toolkit to maintain component independence (decoupling) and maintain reversed dependency flow. You don't have to use Rx in all of your project, but I urge you to consider adding it in situations like this.

Here's how to accomplish this task without Rx:

Scopes

AccountFieldPicker

// AccountFieldPickerViewController.swift

protocol AccountFieldPickerPresentableListener: class {
    func selectOption(_ option: OptionTypeEnum) -> Void
}

final class AccountFieldPickerViewController: UIViewController, AccountFieldPickerPresentable, ... {
    weak var listener: AccountFieldPickerPresentableListener?
    // [...]
    // would the call this when option changes like:
        listener?.selectOption(currentOption)
}

Then propagate that call via listener chain:

// AccountFieldPickerInteractor.swift
public protocol AccountFieldPickerListener: class {
    func activateAccountOption(_ option: String) -> Void
}

final class AccountFieldPickerInteractor: PresentableInteractor<AccountFieldPickerPresentable>, AccountFieldPickerInteractable, AccountFieldPickerPresentableListener {
    weak var listener: AccountFieldPickerListener?
    // [...]
    // MARK: AccountFieldPickerPresentableListener
    func selectOption(_ option: OptionTypeEnum) {
        // TODO: Do necessary logic concerning selection.
        listener?.activateAccountOption(option.rawValue) // converting to primitive type unless enum type is shared
    }

AccountField

// AccountFieldInteractor.swift
protocol AccountFieldRouting: Routing {
    func rebuildField(withType type: String) -> Void
}

final class AccountFieldInteractor: /* [...] */ , AccountFieldInteractable {
    // [...]
    //  will implement AccountFieldPickerListener via its Interactable protocol compliance
    // MARK: AccountFieldPickerListener
    func activateAccountOption(_ option: String) {
        // TODO: Perform necessary logic before `rebuildField` will detach the old Field
        router?.rebuildField(withType: option)
    }
}
// AccountFieldRouter.swift
protocol AccountFieldInteractable: Interactable, AccountFieldPickerListener, AccountFieldInputListener {
    // [...]
}

final class AccountFieldRouter: Router<AccountFieldInteractable>, AccountFieldRouting {
    // [...]
    // MARK: AccountFieldRouting
    func rebuildField(withType type: String) {
        detachCurrentField()
        attachField(withType: type)
    }

    // [...]

    // MARK: - Private
    private let accountFieldInputBuilder: AccountFieldInputBuildable // initialize in routing constructor
    private let currentField: ViewableRouting?
    func detachCurrentField() {
         // TODO: Implement detachment logic
         fatalError("must implement")
    }
    func attachField(withType type: String) {
        let input = accountFieldInputBuilder.build(withListener: interactor, fieldType: type)
        self.currentField = input // should be previously detached or leaks
        attachChild(input)
        // you are not switching to input, but just injecting it into VC hierarchy — define this logic in parent VC
        viewController.addViewController(input.viewControllable) 
    }
}

These are obviously very naive samples but they should get you on the right track.

Guys & gals from @uber should be a lot of help commenting on what I outlined above and catching possible mistakes (I'm a novice as well).

neakor commented 5 years ago

Sorry for getting to this so late. Reading the original question, it sounds like there's a dynamic dependency that needs to be passed from a parent to a child. In this case, there are two options.

One we can create a data holder, such as an Rx stream, at the parent scope. This stream can then be DI down to the child. The parent can inject data into this stream and the child can consume by subscribing.

The second option is to pass the data view the child builder's build method. Something like childBuilder.build(with: someData). Inside the build method the data can be constructor injected to the appropriate unit.