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

Usage help: switch-based navigation #70

Closed hatched-cory closed 2 years ago

hatched-cory commented 2 years ago

I am making this issue to ask for advice, this is not a bug report :)

I'm trying to wrap my head around this library to see if we can use it in production apps. I think it's brilliant but its so different from what I am used to that my brain is a little rusty.

Here's an example app structure I have:

key window ->
    tab bar controller ->
        [tab 1] nav controller -> home controller
        [tab 2] nav controller -> circle controller

I have another controller, the search controller, that I can present from either the home or circle controller. here are my routes:

static let homeTabFactory = CompleteFactoryAssembly(factory: NavigationControllerFactory<UINavigationController, Any?> {
        $0.tabBarItem.title = "Home"
        $0.tabBarItem.image = UIImage(systemName: "house")
    })
    .with(ClassFactory<HomeController, Any?>())
    .assemble()

static let circleTabFactory = CompleteFactoryAssembly(factory: NavigationControllerFactory<UINavigationController, Any?> {
        $0.tabBarItem.title = "Circle"
        $0.tabBarItem.image = UIImage(systemName: "circle")
    })
    .with(ClassFactory<CircleController, Any?>())
    .assemble()

static let mainTabBarFactory = CompleteFactoryAssembly(factory: TabBarControllerFactory())
    .with(homeTabFactory)
    .with(circleTabFactory)
    .assemble()

static let boot = StepAssembly(
        finder: ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root),
        factory: mainTabBarFactory
    )
    .using(GeneralAction.replaceRoot())
    .from(GeneralStep.root())
    .assemble()

static let home = StepAssembly(
        finder: ClassFinder<HomeController, Any?>(),
        factory: NilFactory() // the home controller is initted at boot and only 1 ever exists
    )
    .from(boot)
    .assemble()

static let search = StepAssembly(
        finder: ClassFinder<SearchController, String?>(),
        factory: ClassFactory<SearchController, String?>()
    )
    .using(UINavigationController.push())
    .from(home.expectingContainer())
    .assemble()

my question involves presenting the SearchController. I have the step search setup to do only part of what I want:

I think I need to use SwitchAssembly to pull it off but I'm unable to write anything that will even compile. I feel like I should also be able to compose with the home step above, but I am coming up blank.

appreciate the help in advance!


🎉 UPDATE: I think I have written a step that does exactly what I want, however I feel like it might be a bit clunky. In particular, I feel as if I'm cheating when using .expectingContainer() - but maybe that's just because I have not developed a good intuition on when it is truly needed.

This is what I came up with:

static let searchAlt = StepAssembly(
        finder: ClassFinder<SearchController, String?>(options: .currentVisibleOnly, startingPoint: .topmost),
        factory: ClassFactory<SearchController, String?>()
    )
    .using(UINavigationController.push())
    .from(searchNavigationFinder.expectingContainer())
    .assemble()

static let searchNavigationFinder = SwitchAssembly<UINavigationController, Any?>()
    .addCase(
        when: ClassFinder<HomeController, Any?>(options: .currentVisibleOnly, startingPoint: .topmost),
        from: home.expectingContainer()
    )
    .assemble(default: ChainAssembly
        .from(NavigationControllerStep())
        .using(GeneralAction.presentModally(presentationStyle: .automatic))
        .from(GeneralStep.current())
        .assemble()
    )
ekazaev commented 2 years ago

Hi @hatched-cory

Sorry for the late reply. I am on the short break from work.

I cant exactly check your code right now, but I can only play it in my head. But it looks valid to do what you expect.

Few notes though:

  1. if the search controller is visible on screen, just use that and change its context If you are using default Finder and Factory - youll meet either use https://ekazaev.github.io/route-composer/Structs/ContextSettingTask.html to actually set new context in your search view controller or write custom finder (not recommended in this case)
  2. static let searchNavigationFinder = SwitchAssembly<UINavigationController, Any?>() do not call this step - Finder. You are mixing the concepts together. It may confuse you in the future. Finder just finds the view controller in the stack. Whilest here it is the actual step you can actually navigate to and use it in the chain.
  3. from(searchNavigationFinder.expectingContainer()) I have the feeling that expecting container is redundant here as searchNavigationFinder step already contains information that there will be UINavigationController Involver. But what necessary is adaptingContext instead to tell the compiler that String is actually Any?.

Please let me know if I can help you more.

hatched-cory commented 2 years ago

@ekazaev thank you so much for your reply! Your advice was extremely helpful and is working great. I am feeling more and more confident with this library - and more confident that it is so much better than old-style coordinator pattern. Thanks so much for writing this code :)

One last question - what is the recommended way of setting up steps that don't have a context or rather don't care about context? As you can see above I've been using Any? in these scenarios.

ekazaev commented 2 years ago

@hatched-cory you are very welcome. Yep its a bit tricky at the beginning but simplifies thing a lot. Just uses different abstractions. And yes. If you want to say that this particular step doesn’t care about context you need to declare it as Any? Which can represent both any value or nil.

ekazaev commented 2 years ago

@hatched-cory I am closing this issue as it seems you are satisfied with the answer. Feel free to reopen it or a create a new one if you have other questions. Best of luck