Open JulianHunt opened 5 years ago
Use RxSwift, ability to mutate reference types, or rebuild the rib using the builder preserved in the parent router.
Trying to find a solution without Rx, team is to junior for adding Rx to be viable right now.
@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:
AccountFieldPicker
is the RIB A — spinner picker allowing the user to select one of the account fields.AccountFieldInput
is the RIB B — the VC that allows entry of individual pieces of information; AccountField
is the parent RIB that builds A and B
Please consider that as the number of fields grows — the B RIB VC will become bloated thus it should really be a plugin point, or similar abstraction that would decouple
show field of type X
logic fromdisplay field of type X and handle user interaction
logic. If doing so RIB B would build child RIBs when user selects the type. I would actually advise going this route, but I will still outline the example below.
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).
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.
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.