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

When and why to define Steps as computed property, static var, static func and let #85

Closed
bennnjamin opened this issue Oct 27, 2022 · 7 comments
Assignees
Labels
question Further information is requested

Comments

@bennnjamin
Copy link

I am new to this library and looking through the Example Project is a great place to start, it's very well documented. One thing that I have had trouble understanding and doesn't seem obvious is why Steps are defined in a variety of ways. Sometimes they are static vars/funcs, sometimes they are computed properties, and sometimes they are let constants.

What is your thinking in using these different approaches to defining routes? Why shouldn't every route just be static let so they can refer to each other if needed, and be accessible from every view controller?

@ekazaev
Copy link
Owner

ekazaev commented Oct 28, 2022

@bennnjamin

TBH the Example project is just a demo, not an example of some kind of best practice. For me it was important to show that you can store the routes in variables, they can be dynamically updated and so on.

I presonally create a protocol that I call Wireframe as basically entire app is dependent on it. Something like:

typealias RoutingCompletionBlock = (_: RoutingResult) -> Void

protocol Wireframe: AnyObject {
    var sharedWireframe: SharedWireframe { get }
    var searchWireframe: SearchWireframe { get }
    var moreWireframe: MoreWireframe { get }
    var patientWireframe: PatientWireframe { get }

    func showStudySelection(with context: StudyContext, completion: RoutingCompletionBlock?)
    func showTeamSelect(with context: TeamSelectContext, completion: RoutingCompletionBlock?)
    func showTeamDetail(with context: TeamDetailsListContext, completion: RoutingCompletionBlock?)
    ....
    func showAlertsAttachment(with context: AlertsAttachmentFactory.Context, completion: RoutingCompletionBlock?)
}

It may have some smaller wireframes inside for the smaller parts of the app. And then I implement all the show... methods in the class that extends this protocol and holds the instance of the router. And then I pass this global or smaller wireframe to the factories that build the view controllers and its view models (whatever) so they all know about the wireframe and can use it for their internal navigation. It also allows you to have some kinda TestWireframe that implements the same protocol that you can use in unit tests when testing those view models (or whatever).

@ekazaev ekazaev added the question Further information is requested label Oct 28, 2022
@ekazaev ekazaev self-assigned this Oct 28, 2022
@bennnjamin
Copy link
Author

Yeah I am definitely looking for a best practices approach as I would like to use this in a medium-scale production app.

The protocol approach seems good but I have a couple questions on some of the specifics:

  1. Do you still define your Steps/DestinationStep objects in a central Configuration file, or are they defined in the protocol implementation? If they are in the protocol implementation then how do you navigate between wholly separate navigation flows (ex. between nested routes within Wireframes)?
  2. Can you explain a little bit more about how using .from(homeScreen) to reference a previously defined, usually static step works? What does it mean when you reference another DestinationStep instead of something like .current() as far as Router parsing?

@ekazaev
Copy link
Owner

ekazaev commented Oct 28, 2022

Hi @bennnjamin

  1. I usually define steps right in the implementation of the method in the class that implements the Wireframe. Something like this:
final class DefaultWireframe: Wireframe {
    private let router: Router ....
    ....
    func showTeamDetail(with context: TeamDetailsListContext, completion: RoutingCompletionBlock?) {
        let resultStep = StepAssembly(finder: ClassWithContextFinder(), factory: TeamDetailsListFactory(configuration: .init(payloadHolder: sessionPayloadHolder, networkManager: NetworkManager.shared, wireframe: self)))
            .using(MainNavigationController.push())
            .from(NavigationControllerStep())
            .using(GeneralAction.presentModally(presentationStyle: nil))
            .from(GeneralStep.current())
            .assemble()
        router.commitNavigation(to: Destination(to: resultStep, with: context), animated: true, completion: completion)
    }
    ....
}

@ekazaev
Copy link
Owner

ekazaev commented Oct 28, 2022

  1. Yes. You can reference another destination steps. .current() basically returns you a custom destination step. Have a look into the source code and youll see this:
    public static func current<C>(windowProvider: WindowProvider = RouteComposerDefaults.shared.windowProvider) -> DestinationStep<UIViewController, C> {
        DestinationStep(CurrentViewControllerStep(windowProvider: windowProvider))
    }

But you can use any other destination step and build longer chain.

Why it can be needed? Like when you want to present something from some particular view controller. Not from just current.

Let say you have 2 view controllers. One is a description of the hotel and another is a description of the room in this hotel. So when let say user receives a push notification that has a room id you want to push both view controllers into a navigation view controller. There it is usefull to use such chain. NB: Keep in mind those factories should use the same Context type.

Lets look into the example with . from(homeScreen)

    var squareScreen: DestinationStep<SquareViewController, Any?> {
        StepAssembly(
            finder: ClassFinder<SquareViewController, Any?>(),
            factory: NilFactory())
            .from(homeScreen)
            .assemble()
    }

    var homeScreen: DestinationStep<UITabBarController, Any?> {
        StepAssembly(
            finder: ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root),
            factory: StoryboardFactory(name: "TabBar"))
            .using(GeneralAction.replaceRoot(animationOptions: .transitionFlipFromLeft))
            .from(GeneralStep.root())
            .assemble()
    }

the code above basically represents next:

    var squareScreen: DestinationStep<SquareViewController, Any?> {
        StepAssembly(
            finder: ClassFinder<SquareViewController, Any?>(),
            factory: NilFactory())
            .from(StepAssembly(
                    finder: ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root),
                    factory: StoryboardFactory(name: "TabBar"))
                    .using(GeneralAction.replaceRoot(animationOptions: .transitionFlipFromLeft))
                    .from(GeneralStep.root())
                    .assemble())
            .assemble()
    }

that can be flattened something like this:

I flattened it just for simplicity and to explain how flexible can be your chain configuration. There are multiple ways to write the same chain.

    var squareScreen: DestinationStep<SquareViewController, Any?> {
        StepAssembly(
            finder: ClassFinder<SquareViewController, Any?>(),
            factory: NilFactory())
            .from(SingleStep(finder: ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root), factory: StoryboardFactory(name: "TabBar")))
            .using(GeneralAction.replaceRoot(animationOptions: .transitionFlipFromLeft))
            .from(GeneralStep.root())
            .assemble()
    }

SquareViewController is a view controller within a UITabBarController. So to "go" to it we need to make it visible in that tab bar if it is not. And to do so we need to find it. This is why ClassFinder< SquareViewController, Any?>. Ok. What happens if router did not find it? It moves to next step whis is homeScreen. It uses ClassFinder<UITabBarController, Any?>(options: .current, startingPoint: .root) to find a UITabBarController in the root. Let say router did not find it either. Let say because you are on the welcome screen. Router goes to the next step and it finds the root view controller (it always exists in the app). So he starts to go back by the chain and use factories to build what is missing. So it uses StoryboardFactory(name: "TabBar") to build the tab bar. Keep in mind that the UITabBarController will be already built with both CircleViewController and SquareViewController, knowing that we used the NilFactory in the first step. NilFactory doesnt build anything. Instead it just checks that the desired view controller is in place. And then router checks that the entire chain is visible. So as the SquareViewController was not visible originally as it is in the second tab router will make it visible. And switch the tab.

Basically this gif explains what is happening there:
1*CyRNLBTCZGlXF707NVUuqA

I hope it expalins you why you may want a longer chain.

I would suggest to read this article:
https://itnext.io/coordinator-patterns-issues-what-is-routecomposer-8b50a0477917

@bennnjamin

@bennnjamin
Copy link
Author

Thanks, this is a really thorough explanation. I will need to do that exact example that you mentioned with showing a specific tab in a tab bar from anywhere in the app. I will work on it more this week and reach out if I have any more questions I like your approach a lot and will try to do something similar using the Wireframes.

@bennnjamin
Copy link
Author

That article was actually how I found this project. I was looking at RxFlow and saw some stale github issues and your article completely convinced that this is the best navigation library for iOS.

@ekazaev
Copy link
Owner

ekazaev commented Oct 31, 2022

@bennnjamin Happy to help. Ill close this issue then. Feel free to reopen it or create a new one. Best of luck.

@ekazaev ekazaev closed this as completed Oct 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants