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

What is the best way to handle different routes for the same deep link? #89

Closed bennnjamin closed 1 year ago

bennnjamin commented 1 year ago

Hi, I am trying to setup a deep linking flow that is different based on some state, like if the user is logged in.

The deep link contains the unique identifier for a chat room for example. I would like this route flow:

If the user is logged in, take them to the chat room. Else, show the user the sign up screen to create an account. Is there a way to handle this exclusively using Route Interceptors and different routes? Or do I simply check if they are logged in when handling deep links and then navigate accordingly?

I already have a Route Interceptor to check if the user is logged in and take them to the Login screen if they aren't logged in (the login screen can also route the user to the sign up screen to create an account), but I am not sure how to integrate deep link logic into this so that there are entirely different routes the user takes from the same deep link.

ekazaev commented 1 year ago

@bennnjamin Hi, Sorry for the late reply. I was away.

If I understand you correctly, that situation is covered in the Example app. Id use the routing interceptor to present user a login screen and signup screen within that if necessary. It will give you an easy ability to navigate user to the destination after he signs up as you will have to just call a success of that interceptor and user will continue the navigation that user originally. You'll probably want to have a separate router instance to use within the interceptor so it wont interfere with the original router.

Let me know if that is enough tip for you. Otherwise we can look at the situation closer

bennnjamin commented 1 year ago

I think this more of an application logic issue. The deeplink contains data to route them to the correct destination if the user is logged in.

I am struggling with routing to a nested Destination when the user is not logged in. Login Interceptor can only route the user to the Login Screen when not logged in, but I need to send the user to a nested Destination (Login Screen -> Sign Up Screen). Is there a way to handle this with an interceptor or router? Or do I need to implement this in my own application logic?

ekazaev commented 1 year ago

@bennnjamin I dont fully understand then what exactly you struggle with. You can write a configuration for login interceptor to send user on any screen. The same like your usual router can send user to any screen and can have it is own router to navigate withing login/signup flow if necessary. Do you want to have a call? I think I dont fully understand your problem.

bennnjamin commented 1 year ago

Ok I understand now. I didn't realize LoginInterceptor can have it's own router. Is there a short example of this somewhere? For example, if the user didn't click a deep link, LoginInterceptor will navigate to a default route like the Login Screen, but if the user clicked a deep link then LoginInterceptor will navigate to another route like Sign Up screen.

ekazaev commented 1 year ago

@bennnjamin Sorry for the late reply. I dont have the exact example for you unfortunately.

You can create a protocol for the context and make your global indicator something like this ( I provided pieces of my code - and I dont have such a complex example in my project):

    // Here we are using a new router as we are creating another navigation within main navigation process, so we dont want any interference
    // from the setting and limitations that are applied to to the main Router.
    let internalRouter = DefaultRouter(logger: DefaultLogger(.verbose, osLog: OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "StudySwitchingInterceptorRouter")))

final class StudySwitchingInterceptor: RoutingInterceptor {
    func perform(with context: Any?, completion: @escaping (_: RoutingResult) -> Void) {
        // If context doesnt support study information - just continue navigation
        guard let studyIdentifyingContext = context as? StudySwitchingInterceptorSupporting else {
            completion(.success)
            return
        }
       ....
            // ... then present loading view controller and setup the studyStateManager (is is an async process)
            let resultStep = StepAssembly(
                finder: NilFinder(),
                factory: StudyLoadingFactory(completion: { result in
                    switch result {
                    case let .success(state):
                        // If study switching happened successfully - present new Tab Bar with this study.
                        self.showTabBarController(for: state.study.identifier, completion: completion)
                    case let .failure(error):
                        self.handleError(error, completion: completion)
                    }
                })
            )
            .using(MainNavigationController.pushAsRoot())
            .from(GeneralStep.custom(using: GlobalNavigationControllerFinder<Any?>()).adaptingContext())
            .assemble()
            internalRouter.commitNavigation(to: Destination(to: resultStep, with: StudyLoadingContext(identifier: contextStudyIdentifier)), animated: true, completion: { result in
                switch result {
                case .success:
                    break
                case let .failure(error):
                    self.handleError(error, completion: completion)
                }
            })
...

    private func showTabBarController(for identifier: StudyIdentifier, completion: @escaping (_: RoutingResult) -> Void) {
        updateDefaults(with: identifier)
        let isPatient = try? userStateManager.getCurrentUserManager().user.isPatient
        let context = TabBarFactoryContext(studyType: .exact(identifier), isPatient: isPatient ?? false)
        if context.isPatient {
            let resultStep = StepAssembly(
                finder: NilFinder(),
                factory: PatientTabBarFactory(sessionPayloadHolder: sessionPayloadHolder)
            )
            .using(MainNavigationController.pushAsRoot())
            .from(GeneralStep.custom(using: GlobalNavigationControllerFinder<Any?>()).adaptingContext())
            .assemble()
            internalRouter.commitNavigation(to: Destination(to: resultStep, with: context), animated: false, completion: completion)
        } else {
            let resultStep = StepAssembly(
                finder: NilFinder(),
                factory: MainTabBarFactory(sessionPayloadHolder: sessionPayloadHolder)
            )
            .using(MainNavigationController.pushAsRoot())
            .from(GeneralStep.custom(using: GlobalNavigationControllerFinder<Any?>()).adaptingContext())
            .assemble()
            internalRouter.commitNavigation(to: Destination(to: resultStep, with: context), animated: false, completion: completion)
        }
    }
   }

And expose from the context if that context was created from the deeplink or not. And act in the Interceptor accordingly presenting either login or signup screen using it internal router. When user succesfully signins or signups - return the control to the interceptor using some kind of delegation so it will call its completion method. Then the main router will process with the original route.

Basically my Interceptor does next thing. User may have access to a multiple studies. If they tap on a link that doesnt belong to this study - interceptor builds StudyLoadingViewController using StudyLoadingFactory which just shows the loading indicator but loads and setups the environment behind. Then it retruns control back to the interceptor. It then builds a default tab bar with the entire invironment for the new study - and then default router can proceed with the deeplink after switching the study. In your case you probably want to share the internalRouter with the entire Login/Signup configuration so that regular router wont interfere.

bennnjamin commented 1 year ago

Thanks for your really detailed example. I will definitely integrate something similar in my project and let you know if I have any questions.

I just wanted to add that this usage of interceptors and multiple routers is incredibly powerful. I am very impressed with this library.