Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to change root view controller using LoginInterceptor and then continue navigation #82

Closed
tadelv opened this issue Jun 2, 2022 · 13 comments
Assignees
Labels
help wanted Extra attention is needed

Comments

@tadelv
Copy link

tadelv commented Jun 2, 2022

Hi!

I am trying to understand how to compose the navigation for the following case:

  • user taps on deep link outside the app (cold boot) to a destination behind a login interceptor
  • app opens on splash (landing) and navigates to login (due to interceptor)
  • login succeeds, root is changed from splash to home
  • destination is shown

I've tried to navigate to home in the interceptor, but the subsequent navigation to destination fails, because landing was used for origin view controller.

Any ideas?

P.S.: I'm trying to do this in the example app with the ColorViewController. Here is the diff:

index 12d2bee8..c42e0b3f 100644
--- a/Example/RouteComposer/Configuration/ExampleConfiguration.swift
+++ b/Example/RouteComposer/Configuration/ExampleConfiguration.swift
@@ -78,6 +78,7 @@ extension ExampleScreenConfiguration {
         StepAssembly(
             finder: ColorViewControllerFinder(),
             factory: ColorViewControllerFactory())
+            .adding(LoginInterceptor<String>())
             .adding(DismissalMethodProvidingContextTask(dismissalBlock: { context, animated, completion in
                 // Demonstrates ability to provide a dismissal method in the configuration using `DismissalMethodProvidingContextTask`
                 UIViewController.router.commitNavigation(to: GeneralStep.custom(using: PresentingFinder()), with: context, animated: animated, completion: completion)
index 97e20638..0a3dff78 100644
--- a/Example/RouteComposer/SceneDelegate.swift
+++ b/Example/RouteComposer/SceneDelegate.swift
@@ -18,6 +18,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
     func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
         ConfigurationHolder.configuration = ExampleConfiguration()
 
+      guard let windowScene = (scene as? UIWindowScene) else { return }
+
+      /// 2. Create a new UIWindow using the windowScene constructor which takes in a window scene.
+      let window = UIWindow(windowScene: windowScene)
+
+      let storyboard = UIStoryboard(name: "PromptScreen", bundle: nil)
+      let controller = storyboard.instantiateInitialViewController()
+
+      window.rootViewController = controller
+      self.window = window
+      
         // Try in mobile Safari to test the deep linking to the app:
         // Try it when you are on any screen in the app to check that you will always land where you have to be
         // depending on the configuration provided.
@@ -26,8 +37,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         // dll://products?product=01
         // dll://cities?city=01
         ExampleUniversalLinksManager.configure()
+
+      window.makeKeyAndVisible()
@tadelv
Copy link
Author

tadelv commented Jun 2, 2022

I forgot to add that i would like the destination to open from either the circle view controller or from anywhere the user is currently on (and logged in)

@ekazaev ekazaev self-assigned this Jun 2, 2022
@ekazaev ekazaev added the help wanted Extra attention is needed label Jun 2, 2022
@ekazaev
Copy link
Owner

ekazaev commented Jun 2, 2022

@tadelv

Thank you for the question. I am not able to reply right now as I am not by the computer. Ill try to answer your question asap.

@tadelv
Copy link
Author

tadelv commented Jun 2, 2022 via email

@tadelv
Copy link
Author

tadelv commented Jun 3, 2022

@ekazaev I think I have found a way to do it. Find the diff below. Is this the right way to go about it?
I am using a SwitchAssembly to determine the source controller based on the login status of the user.

index 12d2bee8..51e6887a 100644
--- a/Example/RouteComposer/Configuration/ExampleConfiguration.swift
+++ b/Example/RouteComposer/Configuration/ExampleConfiguration.swift
@@ -78,6 +78,7 @@ extension ExampleScreenConfiguration {
         StepAssembly(
             finder: ColorViewControllerFinder(),
             factory: ColorViewControllerFactory())
+            .adding(LoginInterceptor<String>())
             .adding(DismissalMethodProvidingContextTask(dismissalBlock: { context, animated, completion in
                 // Demonstrates ability to provide a dismissal method in the configuration using `DismissalMethodProvidingContextTask`
                 UIViewController.router.commitNavigation(to: GeneralStep.custom(using: PresentingFinder()), with: context, animated: animated, completion: completion)
@@ -86,7 +87,16 @@ extension ExampleScreenConfiguration {
             .using(ExampleNavigationController.push())
             .from(SingleContainerStep(finder: NilFinder(), factory: NavigationControllerFactory<ExampleNavigationController, String>()))
             .using(GeneralAction.presentModally())
-            .from(GeneralStep.current())
+            .from(SwitchAssembly<UIViewController, String>()
+              .addCase({ _ in
+                guard isLoggedIn == false else {
+                  return nil
+                }
+                return circleScreen.unsafelyRewrapped()
+              })
+                .assemble(default: {
+                  GeneralStep.current()
+                }))
             .assemble()
     }

@tadelv
Copy link
Author

tadelv commented Jun 13, 2022

Hey @ekazaev, just pinging, are you still away from the computer? :)

@ekazaev
Copy link
Owner

ekazaev commented Jun 13, 2022

Hey. Sorry. I am on vacation. Will be back tomorrow 🥹

@tadelv
Copy link
Author

tadelv commented Jun 13, 2022 via email

@ekazaev
Copy link
Owner

ekazaev commented Jun 15, 2022

@tadelv Sorry for the delay.
If I understood your question correctly here you hit a limitation of the logic. It first searches for the view controller to start as configuration may be incorrect or you can be already on the screen that requires login (and if you are already there it is pointless to show login right?), then if it found the appropriate view controller to start it runs all the gloably assigned interceptors and then individual interceptors. And continues to build the leftover route after all the Interceptors succeded.

The problem here is that you can't change what router considers as a partialy build stack as correct from the interceptor. For example, if you are on HomePage as origin, you want to navigate somewhere but in the Interceptor you want to change the origin as well. I did not implement it as it will make the router logic to complex, hard to explain and hard to debug. But it is also a rare case as for the intermediate login full screen modal is usually used and then navigation continues.

But if it is necessary, the GlobalInterceptorRouter wrapper can help you. You can add your interceptor to it and it will run an interceptror before the main router starts to work. Then only thing is you can not parse the configuration from that wrapper so youll have to mark all the contexts that require to have a login somehow. For example with an empty protocol LoginRequiring.

Then you can write your global interceptor like

    func perform(with context: Any?, completion: @escaping (_: RoutingResult) -> Void) {
        guard let loginRequiringContext = context as? LoginRequiring else {
            completion(.success)
            return
        }
        // Otherwise check if user is logged in and if not - do whatever you want with the stack as the MainRouter hasnt started to work yet

Or viceversa if most of the screens require login and only a few dont, mark them with a protocol NotRequiringLogin and update your GlobalLoginInterceptor accordingly.


Another way (complex as it is not provided by the library and requires coding) is to make your own wrapper to the Router as it has only one method.

func navigate<ViewController: UIViewController, Context>(to step: DestinationStep<ViewController, Context>,
                                                             with context: Context,
                                                             animated: Bool,
                                                             completion: ((_: RoutingResult) -> Void)?) throws

In this method you can save the configuration that was passed there. Run the configuration through the DefaultRouter, throw the your own Error from the LoginInterceptor in case user is not logged in. That Error you can recognise in your wrapper, present user login screen from the wrapper, and if user successfully logins there within the wrapper change the view controllers stack and then run the saved configuration through the DefaultRouter again.


Another way is to make your configuration like you wrote it, but i think it may limit or make complex your configurations in the future. I would go with one of the solutions above.

Hope I understood your question correctly. Please let me know.

@tadelv
Copy link
Author

tadelv commented Jun 17, 2022

Hey @ekazaev thanks for taking the time to write this exhaustive answer.

Let me see if I got it right: The global interceptors run before router decides which will be the originating (root) view controller? So this means, they can change the view controller hierarchy before origin VC is selected?
I assume this will definitely be cleaner than the current solution. But on the other hand, I could package the SwitchAssembly into a property of the configuration and just use it where i need it - though it will still introduce complexity which would be abstracted away by the global interceptor.
The app I'm planning to build using route-composer has most of the screens behind login, but some of them require a specific navigation stack to be built and some can be presented on any current view controller visible (when authenticated), i.e. a certain detail view will require to have a list view in the stack, but a certain other detail view will be presented modally anywhere, requiring only that the user is logged in (and the correct rootVC is there).
The app is divided into two states (which have their own rootVCs), logged in and not logged in. In normal operation, the user starts with the not-logged-in state and after authenticating, the rootVC is changed (let's say to a tab bar view controller).

Hope it makes sense. Let me know please if my assumptions regarding global interceptor are correct

@tadelv
Copy link
Author

tadelv commented Jun 17, 2022

Just a quick update - I was able to get it to work the way you proposed, using a GlobalInterceptor and replacing the root view controller in there.

index 31ddbf30..dda355c3 100644
--- a/Example/RouteComposer/Extensions/ViewController.swift
+++ b/Example/RouteComposer/Extensions/ViewController.swift
@@ -11,6 +11,8 @@ import os.log
 import RouteComposer
 import UIKit
 
+protocol RequiresLogin {}
+
 extension UIViewController {
 
     // This class is needed just for the test purposes
@@ -29,8 +31,53 @@ extension UIViewController {
         }
     }
 
+
+
+  private final class GlobalLoginInterceptor<C>: RoutingInterceptor {
+    typealias Context = C
+
+    func perform(with context: Context, completion: @escaping (RoutingResult) -> Void) {
+      guard context is RequiresLogin else {
+        completion(.success)
+        return
+      }
+      guard isLoggedIn == false else {
+        completion(.success)
+        return
+      }
+      let destination = LoginConfiguration.login()
+      do {
+        try UIViewController.router.navigate(to: destination) { routingResult in
+          guard routingResult.isSuccessful,
+                let viewController = ClassFinder<LoginViewController, Any?>().getViewController() else {
+            completion(.failure(RoutingError.compositionFailed(.init("LoginViewController was not found."))))
+            return
+          }
+
+
+          viewController.interceptorCompletionBlock = { result in
+            guard case .success = result else {
+              completion(result)
+              return
+            }
+            do {
+              try viewController.router.navigate(to: ExampleConfiguration().homeScreen, animated: false) { result in
+                completion(result)
+              }
+            } catch {
+              completion(.failure(error))
+            }
+          }
+        }
+      } catch {
+        completion(.failure(RoutingError.compositionFailed(.init("Could not present login view controller", underlyingError: error))))
+      }
+    }
+  }
+
     static let router: Router = {
         var defaultRouter = GlobalInterceptorRouter(router: FailingRouter(router: DefaultRouter()))
+      defaultRouter.addGlobal(GlobalLoginInterceptor<Any?>())
         defaultRouter.addGlobal(TestInterceptor("Global interceptors start"))
         defaultRouter.addGlobal(NavigationDelayingInterceptor(strategy: .wait))
         defaultRouter.add(TestInterceptor("Router interceptors start"))```

@ekazaev
Copy link
Owner

ekazaev commented Jun 26, 2022

@tadelv are you happy with the result?
I am really sorry for the late replies. I have nine weddings to attend this year so my availability is terrible 🥹

@tadelv
Copy link
Author

tadelv commented Jun 27, 2022

Hey @ekazaev,
I think I have what I need. I was only researching the approaches in the example app in this repo. When I start using route-composer in the production app, I will decide whether to use the configuration approach or the GlobalInterceptor. I think it will depend whether I will have many configurations and whether I can provide different contexts for the interceptor.
But for now, my questions have been answered, thanks a lot!

Good luck with all the weddings 😆

@ekazaev
Copy link
Owner

ekazaev commented Jun 28, 2022

@tadelv Thank you. Ill close this issue then. Dont hesitate either to reopen it or to create a new one

@ekazaev ekazaev closed this as completed Jun 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants