Closed tadelv closed 2 years ago
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)
@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.
Thank you, There is no rush. I am just getting to know this framework, so it might be I am trying to tackle this problem from the wrong angle. Let me know if you need any additional clarification.
On Thu, 2 Jun 2022 at 17:16, Eugene Kazaev @.***> wrote:
@tadelv https://github.com/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.
— Reply to this email directly, view it on GitHub https://github.com/ekazaev/route-composer/issues/82#issuecomment-1144985742, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAK35N7ECWIBQLZYPPOBPLVNDF4XANCNFSM5XVDYT5A . You are receiving this because you were mentioned.Message ID: @.***>
@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()
}
Hey @ekazaev, just pinging, are you still away from the computer? :)
Hey. Sorry. I am on vacation. Will be back tomorrow 🥹
Ooops! Sorry for bothering you then 🙈
On Jun 13, 2022, at 6:04 PM, Eugene Kazaev @.***> wrote:
Hey. Sorry. I am on vacation. Will be back tomorrow 🥹
— Reply to this email directly, view it on GitHub https://github.com/ekazaev/route-composer/issues/82#issuecomment-1154105324, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAK35PHM47ZFDZPMPHDAPTVO5LXZANCNFSM5XVDYT5A. You are receiving this because you were mentioned.
@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.
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
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"))```
@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 🥹
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 😆
@tadelv Thank you. Ill close this issue then. Dont hesitate either to reopen it or to create a new one
Hi!
I am trying to understand how to compose the navigation for the following case:
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: