ekazaev / route-composer

Protocol oriented, Cocoa UI abstractions based library that helps to handle view controllers composition, navigation and deep linking tasks in the iOS application. Can be used as the universal replacement for the Coordinator pattern.
MIT License
896 stars 63 forks source link

Best way to route reused screen #100

Closed Nikitos9I closed 1 year ago

Nikitos9I commented 1 year ago

Hello! I am trying to use your solution and am faced with a misunderstanding how to reuse controller in several navigation chains.

Example1: I have controllers A, B, C, D, E. I want to reuse controller C from A and B.

1) A or B can present C 2) C should present D if was presented from A and should present E from B respectively

Example2: I have controllers A, B, C, D. Note: A and B cannot be inherited from AnyContextCheckingViewController: ContextChecking.

1) A or B can present C 2) C can present D 3) If D was presented so we need to dismiss it to source controller A or B. So we need to pass information about source controller through controller C (reused).

I looked for solution for that case in your demo app, but there are all controllers navigate with concrete configuration. How to resolve this situation in correct way? Thank you!

ekazaev commented 1 year ago

Hi @Nikitos9I I am still on vacation without proper roaming. Ill try to have a look at your case when i am back

ekazaev commented 1 year ago

@Nikitos9I I am back. Trying to understand what you are asking but the examples are a bit vague. It feels that when you mean to reuse the screens it not exactly mean what you wrote in examples.

So I have questions about wha you ask.

  1. When you say that C should present D if presented from A and should present E from B respectively. I assume we are on the same point here that context type of D and E is the same. Lets say it is a productId: String. You just need to select the type of view controller to build. You have multiple ways to go here. You can directly use finder in your configuration function to check which view controller is presented and choose the right configuration based on finder result.

    func goToProduct(with productId: String) {
        if PresentingFinder<Any?>(startingPoint: .custom(ClassFinder<ViewControllerC, Any?>().getViewController()))?.getViewController() is ViewControllerA {
           // Do this configuration
        } else {
           // Do this configuration
        }
    }

    But I have some suspicion that I do not fully understand the use case of that question. Can you be more specific - then Ill be able to give you the right answer. I have the feeling that in reality you have slightly more information about why you need to make a choice between D or E and it is not only based on the who presented who (it also depends on type of presentation, switching a tab in the UITabBarController technically a presentation as well).

  2. This case I dont really understand at all. Dismissal by an action is not exactly a navigation problem. Like swipe back to return to the previous screen has no route-composer involvement in it. If you want to change the behaviour of swipe back interaction you have to deal with swipe back yourself using the UINavigationController delegate. Same applies to swipe down to dismiss the modally presented view controller. I have the feeling that what you are asking is in this area. But again I probably did not understand your question. Can you come back with with a description like I have home page, I want to present product array and then blah. Ideally with the specification of the presentation types. But I use route-composer in various day to day projects and did not find the case that is not covered as soon as it actually the router's responsibility.

PS: Note the way you state the problem based on who presented who may not be correct way to look at things. I think it is easiest to think about navigation from the deep-linking point of view. So lets say your user clicked in the URL with a product id in the Mail and beed redirected to your app. The app could be in the variety of states, it could be not started at all, it could be that user left it being in app settings in that moment choosing an image for the profile from the stadard galery view controller and so on. It is easier to look at it from that point of view. It is highly unlikely that your navigation decisions are based solely on the fact who presented who. And you always need a backup configuration.

Nikitos9I commented 1 year ago

Thank you for answer. I think I understand first part of this.

Lets talk about the second one.

I have

  1. FavouriteProductsController with FavouriteProductsContext
  2. ProductController with ProductContext
  3. SizePickerController with SizePickerContext
  4. SizeSubscribeController with SizeSubscribeContext

It does not matter, how I present FavouriteProductsController or ProductController. In my situation there can be either FavouriteProductsController controller or ProductController on the screen. So on each of them I have button "Pick size", after tapping on which the SizePickerController presents modally for me. Next, I can dismiss it or open SizeSubscribeController from it and after subscription dismiss both controllers SizePickerController and SizeSubscribeController simultaneously.

I can implement several configurations for openning sizePickerController and returning to source. That covers case "I can dismiss it".

Not compiled:

static let sizePickerControllerFromFavourites = StepAssembly<...>(NilFinder(), SizePickerFactory())
    .adding(DismissalMethodProvidingContextTask(dismissalBlock: { (context, animated, completion) in
        UIViewController.router.commitNavigation(to: FavouritesConfiguration.fromSizePicker, with: contextNew, animated: animated, completion: completion)
    }))

static let sizePickerControllerFromProduct = StepAssembly<...>(NilFinder(), SizePickerFactory())
    .adding(DismissalMethodProvidingContextTask(dismissalBlock: { (context, animated, completion) in
        UIViewController.router.commitNavigation(to: ProductConfiguration.fromSizePicker, with: contextNew, animated: animated, completion: completion)
    }))

Next, I would cover case "open SizeSubscribeController from it and after subscription dismiss both controllers SizePickerController and SizeSubscribeController simultaneously".

For this one now I can suggest to send entry point to SizePickerContext and SizeSubscribeContext and after doing stuff inside the screen, I can

switch entryPoint { 
    case .product: UIViewController.router.navigate(...)
    case .favourites: UIViewController.router.navigate(...)
}

I'm not sure if this is the right way, is it possible to solve this purely at the navigation level?

For example, in Coordinators I can start SizePickerController flow with new instance of Router and after in SizeSubscribeController I can call router.dismissToRoot() and it dismisses all controllers next to root.

I hope I explained it more clearly, thank you for helping me

ekazaev commented 1 year ago

@Nikitos9I I think I started to understand it better. I think you are trying then to put to much context into where you want to return while the entry point in your case is not as clearly defined. What I am trying to say is that when you say "In my situation there can be either FavouriteProductsController controller or ProductController on the screen" you mean I dont have a specific entry point but I want to return to it. But you are trying algorithmically describe it later. My suggestion would be to pass a weak reference to the entry point view controller and then navigate to it. If you are using DismissalMethodProvidingContextTask you can perfectly do so as well.

Something like this:

static func sizePicker(with context: Some, from viewController: UIViewContoller) -> DestinationStep<..., ...> {
return StepAssembly<...>(NilFinder(), SizePickerFactory())
    .adding(DismissalMethodProvidingContextTask(dismissalBlock: { [weak viewController] (_, animated, completion) in
        UIViewController.router.commitNavigation(to: GeneralStep.custom(using: InstanceFinder<UIViewController, Any?>(instance: viewController!)), with: nil, animated: animated, completion: completion)
    }))
}

This is probably the easiest way to do so.

Nikitos9I commented 1 year ago

I will try! Thank you

ekazaev commented 1 year ago

@Nikitos9I Hi,

Is there anything else I can help you with related to this issue or it is safe to close it?