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
Why can't the second step in a UINavigationController use a different context than the first step? #86
Comments
@bennnjamin Hi Partially I covered that part in my previous answer. You must use the same context if you are building both view controllers simultaneosly. Like lets say you need to build a hotel view controller and a room view controller within the same navigation controller simulateneously. So in this case youll need both But if you dont, then you dont need to have the same context there. Use But describe me what do you actually want there. Do you want SecondViewController to be pushed after the first one? There are multiple ways of writing the same things as I said. You can write like this for example secondStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<SecondViewController, Any?>())
.using(UINavigationController.push())
.from(GeneralStep.custom(using: ClassFinder<FirstViewController, Any?>()).expectingContainer())
.assemble()
// or
static var secondStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<SecondViewController, Any?>())
.using(UINavigationController.push())
.from(GeneralStep.custom(using: ClassFinder<UINavigationController, Any?>()))
.assemble() Can you understand the difference between those 2 examples? |
Do you mean you are using a top-down approach to pass all necessary variables in the Context from a parent view controller to all child view controllers? For example, if this is a deeply nested navigation controller with 4-5 screens that all require contexts, I would need to pass the same context to each view controller, and each view controller will only use the property in the Context that it needs. Like this:
What if the Navigation Controller may only pushes one view controller at a time, but each view controller requires different context? For example, a Settings Navigation Controller that lets a user edit their profile. When tapping on each profile item (name, phone, password, etc.), a view controller is pushed to change that value, and the context is the current value. Perhaps the Settings Navigation Controller asynchronously fetches this profile information based on its own context (userId) and so those values are not even available to pass as part of a Context object at the time the Settings Controller is created.
I am honestly having difficulty seeing the difference. First of all I do not understand why the second example requires I also do not understand how |
Lets abstract from view controllers. Just theoretically. If you want to show a hotel and a room simultaneously you need this 2 ids at the very same moment. Nothing to do with view controllers. Just logic. Now lets return to what you potentially want to do. You probably want when user clicks on the hotel in the hotel list - present a hotel view controller. And when user clicks on the room - present room view controllers. Those are not simultaneous actions and you dont need that information simultaneously. So having separate context is correct. Now lets return to chains. Why do you try to build a chain of hotel view controller + room view controller? Are you sure you need that? typedef HotelId = UUID
typedef RoomId = String
static var hotelStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<HotelViewController, HotelId>())
.using(UINavigationController.push())
.from(NavigationControllerStep<UINavigationController, HotelId>())
.using(GeneralAction.replaceRoot())
.from(GeneralStep.root())
.assemble()
static var roomStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<RoomViewController, RoomId>())
.using(UINavigationController.push())
.from(hotelStep.expectingContainer()) // Compiler error that String doesn't match UUID but lets ignore that fact.
.assemble()
// User clicks on the room with id "1234"
router.commitNavigation(
to: Destination(to: roomStep, with: "1234"), // No hotel id to find or build `HotelViewController` is provided.
animated: true,
completion: completion
) This is exactly why configuration complains that their context do not match. Do you really need that chain? Let me read for you what this configuration means for router: "If room view controller with this room id doesnt exist then find a hotel view controller with this hotel id (this information is actually missing in context) and push into its navigation controller. But if such hotel view controller doesnt exist build it as well with this hotel id (but there is no hotelId in the context so you will fail for sure)". Given that description of your chain is it what you want? No, most likely what you want is so that your So what you actually want router to do is this: static var roomStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<RoomViewController, RoomId>())
.using(UINavigationController.push())
.from(GeneralStep.current().expectingContainer()) // No dependency on hotel id
.assemble()
// User clicks on the room with id "1234"
router.commitNavigation(
to: Destination(to: roomStep, with: "1234"), // No hotel id is needed because we are happy just to push.
animated: true,
completion: completion
) Ok. Lets say your app is a tab bar application and fisrt tab is a HomeViewController and second tab bar is a UINavigationController. So if user is in the static var roomStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<RoomViewController, RoomId>())
.using(UINavigationController.push())
.from(GeneralStep.custom(using: ClassFinder<UINavigationController, RoomId>()))
.assemble()
// User clicks on the room with id "1234"
router.commitNavigation(
to: Destination(to: roomStep, with: "1234"),
animated: true,
completion: completion
) Here you specify that router must find a So, In real life applications you usually really rarely need to use chains. But they are available for you if needed. But 95% of time you do not need them as you can see from the configurations above. ========
Sorry. my typo So the difference is that the first example the router before pushing the The second example is just searching for any Understanding that difference is very important. Think about deeplinking. You can receive a push notification with some room/hotel/whatever id when user is anywhere on the screen (welcome screen, settings screen etc). Fine tuned configurations allow router to cover any such situation.
Because when you are just referencing the first step you are referencing its entire configuration and its dependencies. So, you tell the router that this step must be there. And if it must be there - then router must have enough information to be able to build the firstStep if it is absent. Thats why compiler complains that their context must be equal. For the reasons explained in the hotel/room example. As I said - 95% of the times you do not need chaining or you are chaining from the view controller that doesn have a context ( Dont worry. It is not the first time someone asks such questions at the beginning. The approach above make you thing about your app differently and think about deeplinking and so on. It takes some times. But then I hope you'll be happy see how much job route-composer can do for you. |
Please read this 2 answers as well. Hope they will give some clarification: It is also important to remember this piece from documentation:
|
Ok I am starting to understand a lot more. I thought that chains were the way to link between specific view controllers but it seems correct way to do that is with I understand the part about the
What about in the case the user receives a push notification and we need to deep link to the room by first presenting the Hotel (by ID). Then these are simultaneous. I saw in the Issue you linked to use I believe these 3 above cases are all related and if I can figure out how to handle them then I should have a good grasp on how to use this library. Thanks for all of your help |
I should also add that the primary reason I was attempting to reference the previous |
Ill try to answer your questions if I understand them correctly: secondStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<SecondViewController, Any?>())
.using(UINavigationController.push())
.from(GeneralStep.custom(using: ClassFinder<FirstViewController, Any?>()).expectingContainer())
.assemble() Of course here we are making an assumption that it exists somewhere in the current view controllers stack. Usually it is so. But if you know that you can be in the situation that it may not exist. Lets say user is on some welcome screen and you dont want to switch the entire app to tab bar until user finishes onboarding. You can use a Ok, lets say you always want to build tab bar for your navigation. Then it most-likely has no context as it should be possible to build it with a big variety of contexts. Let say you also only want to push into the tab with UINavigationController that contains FirstViewController. (Please keep in mind - I dont compile this code, it is here to give you directions) let tabFactory = CompleteFactoryAssembly(factory: TabBarControllerFactory<UITabBarController, Any?>())
.with(CompleteFactoryAssembly(factory: NavigationControllerFactory<UINavigationController, Any?>())
.with(ClassFactory<FirstViewController, Any?>())
.assemble())
.with(CompleteFactoryAssembly(factory: NavigationControllerFactory<UINavigationController, Any?>())
.with(ClassFactory<SettingsViewController, Any?>())
.assemble())
)
.assemble()
let tabBarStep = StepAssembly(
finder: ClassFinder(),
factory: tabFactory)
)
.using(GeneralAction.replaceRoot())
.from(GeneralStep.root())
.assemble()
let secondStep = StepAssembly(
finder: ClassFinder(),
factory: ClassFactory<SecondViewController, Any?>())
.using(UINavigationController.push())
.from(SingleStep(finder: ClassFinder<FirstViewController, Any?>(), factory: NilFactory).expectingContainer())
.using(GeneralAction.nilAction())
.from(tabBarStep)
.assemble()
struct BookingContext {
let hotelId: String
let roomId: String?
} it is well enough described here Do not forget, router no only builds such steps but also needs to check if they are already built. Especially cases like: User opened a deeplink in email and you just pushed both HotelViewController + RoomViewController. User tapped back returned to HotelViewController. Then user returned to the mail app and tapped the deeplink again. Should router build HotelViewController again? No if it is the same hotel and router should only build the RoomViewController and push it. As I said this entire thing is covered, but if you don't have the full understanding yet. Don't concentrate on it. It is an extremely rare case when you need such behaviour. Just trust me that its covered. Overall there is nothing bad to have few configurations in case of deeplinking or normal configuration. All depends on your business tasks. I know apps that if you click on something push them into a navigation controller, but if the request to present such entity comes from deeplinking - they present them modally. So there is nothing wrong to have different configurations to present same view controllers for the different reasons.
I think it is more or less described in the first answer. But again, do you really need it? Do you have doubts that tab bar wont exist when deep-linking happens? Don't forget that the example app uses a lot of advanced configurations for demonstration and testing purposes. But as usual I can only guess your business tasks. But Ill give you an example from one of my projects. Lets say user has access to different medical studies. each medical study is an entire flow with a tab bar. Lets say user is in the study with the |
Thanks for you answer, each time I understand a little bit more. You have some example code that I am a little unfamiliar with that I added the comments. Primarily why is it necessary to use
I have worked on integrating your second example code and it seems to work well to switch between view controllers in a tab bar using the router. It's a good starting point so I will continue to build out more of the controllers and Steps after the tab bar.
I think is the issue for me. I am getting stuck on some rare cases and trying to make sure they are covered. As I look at the required navigation of my app, I believe I will need to use some of these rare cases.
I was trying to avoid presenting them modally, and just build the correct view controller graph as if the user navigated to the view controller manually but I can see that maybe the modal approach is simpler.
This is very similar to my project where the tab bar must be rebuilt if the ID changes so thank you for this example. It's exactly the type of problem I am dealing with. Maybe we can talk directly about some of my specific use cases? |
Just because .from(SingleStep(finder: ClassFinder<FirstViewController, Any?>(), factory: NilFactory).expectingContainer())
.using(GeneralAction.nilAction()) //I am using NilAction that the view controler built by NilFactory doesnt need to be integrated anywhere.
.from(tabBarStep) // Router will not go here if it finds `FirstViewController`. Why to build something if there is enough. Here you basically say build the tab bar if its absent (FirstViewController will be built with the tab bar, right?). Then find
I am not trying to say that presenting modally is the right approach. But as many deep-links I saw - 99% of them works like "push into currently visible
Sure, but lets leave it for the next week. Write me to my email. Have a nice weekend. |
Sounds good, thanks for your help. |
My goal is to setup a navigation controller and have a series of view controllers that I can push onto the navigation controller stack. I feel like I'm missing something obvious because this should be very simple. I'd like to pass a unique identifier
Int?
to the first view controller, instead of usingAny?
as the context.This does not compile, the compiler complains that it can't convert
Int?
toAny
? and thatGeneric parameter 'VC' could not be inferred.
The text was updated successfully, but these errors were encountered: