GithubHelp home page GithubHelp logo

quickbirdeng / xcoordinator Goto Github PK

View Code? Open in Web Editor NEW
2.2K 37.0 173.0 49.18 MB

🎌 Powerful navigation library for iOS based on the coordinator pattern

License: MIT License

Ruby 0.65% Swift 98.82% Shell 0.53%
rxswift rxswift-extensions ios swift ios-swift coordinator coordinator-pattern mvvm mvvm-architecture mvvm-c

xcoordinator's Introduction

Build Status CocoaPods Compatible Carthage Compatible Documentation Platform License

⚠️ We have recently released XCoordinator 2.0. Make sure to read this section before migrating. In general, please replace all AnyRouter by either UnownedRouter (in viewControllers, viewModels or references to parent coordinators) or StrongRouter in your AppDelegate or for references to child coordinators. In addition to that, the rootViewController is now injected into the initializer instead of being created in the Coordinator.generateRootViewController method.

β€œHow does an app transition from one view controller to another?”. This question is common and puzzling regarding iOS development. There are many answers, as every architecture has different implementation variations. Some do it from within the implementation of a view controller, while some use a router/coordinator, an object connecting view models.

To better answer the question, we are building XCoordinator, a navigation framework based on the Coordinator pattern. It's especially useful for implementing MVVM-C, Model-View-ViewModel-Coordinator:

πŸƒβ€β™‚οΈGetting started

Create an enum with all of the navigation paths for a particular flow, i.e. a group of closely connected scenes. (It is up to you when to create a Route/Coordinator. As our rule of thumb, create a new Route/Coordinator whenever a new root view controller, e.g. a new navigation controller or a tab bar controller, is needed.).

Whereas the Route describes which routes can be triggered in a flow, the Coordinator is responsible for the preparation of transitions based on routes being triggered. We could, therefore, prepare multiple coordinators for the same route, which differ in which transitions are executed for each route.

In the following example, we create the UserListRoute enum to define triggers of a flow of our application. UserListRoute offers routes to open the home screen, display a list of users, to open a specific user and to log out. The UserListCoordinator is implemented to prepare transitions for the triggered routes. When a UserListCoordinator is shown, it triggers the .home route to display a HomeViewController.

enum UserListRoute: Route {
    case home
    case users
    case user(String)
    case registerUsersPeek(from: Container)
    case logout
}

class UserListCoordinator: NavigationCoordinator<UserListRoute> {
    init() {
        super.init(initialRoute: .home)
    }

    override func prepareTransition(for route: UserListRoute) -> NavigationTransition {
        switch route {
        case .home:
            let viewController = HomeViewController.instantiateFromNib()
            let viewModel = HomeViewModelImpl(router: unownedRouter)
            viewController.bind(to: viewModel)
            return .push(viewController)
        case .users:
            let viewController = UsersViewController.instantiateFromNib()
            let viewModel = UsersViewModelImpl(router: unownedRouter)
            viewController.bind(to: viewModel)
            return .push(viewController, animation: .interactiveFade)
        case .user(let username):
            let coordinator = UserCoordinator(user: username)
            return .present(coordinator, animation: .default)
        case .registerUsersPeek(let source):
            return registerPeek(for: source, route: .users)
        case .logout:
            return .dismiss()
        }
    }
}

Routes are triggered from within Coordinators or ViewModels. In the following, we describe how to trigger routes from within a ViewModel. The router of the current flow is injected into the ViewModel.

class HomeViewModel {
    let router: UnownedRouter<HomeRoute>

    init(router: UnownedRouter<HomeRoute>) {
        self.router = router
    }

    /* ... */

    func usersButtonPressed() {
        router.trigger(.users)
    }
}

πŸ— Organizing an app's structure with XCoordinator

In general, an app's structure is defined by nesting coordinators and view controllers. You can transition (i.e. push, present, pop, dismiss) to a different coordinator whenever your app changes to a different flow. Within a flow, we transition between viewControllers.

Example: In UserListCoordinator.prepareTransition(for:) we change from the UserListRoute to the UserRoute whenever the UserListRoute.user route is triggered. By dismissing a viewController in UserListRoute.logout, we additionally switch back to the previous flow - in this case the HomeRoute.

To achieve this behavior, every Coordinator has its own rootViewController. This would be a UINavigationController in the case of a NavigationCoordinator, a UITabBarController in the case of a TabBarCoordinator, etc. When transitioning to a Coordinator/Router, this rootViewController is used as the destination view controller.

🏁 Using XCoordinator from App Launch

To use coordinators from the launch of the app, make sure to create the app's window programmatically in AppDelegate.swift (Don't forget to remove Main Storyboard file base name from Info.plist). Then, set the coordinator as the root of the window's view hierarchy in the AppDelegate.didFinishLaunching. Make sure to hold a strong reference to your app's initial coordinator or a strongRouter reference.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    let window: UIWindow! = UIWindow()
    let router = AppCoordinator().strongRouter

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        router.setRoot(for: window)
        return true
    }
}

πŸ€Έβ€β™‚οΈ Extras

For more advanced use, XCoordinator offers many more customization options. We introduce custom animated transitions and deep linking. Furthermore, extensions for use in reactive programming with RxSwift/Combine and options to split up huge routes are described.

πŸŒ— Custom Transitions

Custom animated transitions define presentation and dismissal animations. You can specify Animation objects in prepareTransition(for:) in your coordinator for several common transitions, such as present, dismiss, push and pop. Specifying no animation (nil) results in not overriding previously set animations. Use Animation.default to reset previously set animation to the default animations UIKit offers.

class UsersCoordinator: NavigationCoordinator<UserRoute> {

    /* ... */
    
    override func prepareTransition(for route: UserRoute) -> NavigationTransition {
        switch route {
        case .user(let name):
            let animation = Animation(
                presentationAnimation: YourAwesomePresentationTransitionAnimation(),
                dismissalAnimation: YourAwesomeDismissalTransitionAnimation()
            )
            let viewController = UserViewController.instantiateFromNib()
            let viewModel = UserViewModelImpl(name: name, router: unownedRouter)
            viewController.bind(to: viewModel)
            return .push(viewController, animation: animation)
        /* ... */
        }
    }
}

πŸ›€ Deep Linking

Deep Linking can be used to chain different routes together. In contrast to the .multiple transition, deep linking can identify routers based on previous transitions (e.g. when pushing or presenting a router), which enables chaining of routes of different types. Keep in mind, that you cannot access higher-level routers anymore once you trigger a route on a lower level of the router hierarchy.

class AppCoordinator: NavigationCoordinator<AppRoute> {

    /* ... */

    override func prepareTransition(for route: AppRoute) -> NavigationTransition {
        switch route {
        /* ... */
        case .deep:
            return deepLink(AppRoute.login, AppRoute.home, HomeRoute.news, HomeRoute.dismiss)
        }
    }
}

⚠️ XCoordinator does not check at compile-time, whether a deep link can be executed. Rather it uses assertionFailures to inform about incorrect chaining at runtime, when it cannot find an appropriate router for a given route. Keep this in mind when changing the structure of your app.

🚏 RedirectionRouter

Let's assume, there is a route type called HugeRoute with more than 10 routes. To decrease coupling, HugeRoute needs to be split up into multiple route types. As you will discover, many routes in HugeRoute use transitions dependent on a specific rootViewController, such as push, show, pop, etc. If splitting up routes by introducing a new router/coordinator is not an option, XCoordinator has two solutions for you to solve such a case: RedirectionRouter or using multiple coordinators with the same rootViewController (see this section for more information).

A RedirectionRouter can be used to map a new route type onto a generalized ParentRoute. A RedirectionRouter is independent of the TransitionType of its parent router. You can use RedirectionRouter.init(viewController:parent:map:) or subclassing by overriding mapToParentRoute(_:) to create a RedirectionRouter.

The following code example illustrates how a RedirectionRouter is initialized and used.

class ParentCoordinator: NavigationCoordinator<ParentRoute> {
    /* ... */
    
    override func prepareTransition(for route: ParentRoute) -> NavigationTransition {
        switch route {
        /* ... */
        case .child:
            let childCoordinator = ChildCoordinator(parent: unownedRouter)
            return .push(childCoordinator)
        }
    }
}

class ChildCoordinator: RedirectionRouter<ParentRoute, ChildRoute> {
    init(parent: UnownedRouter<ParentRoute>) {
        let viewController = UIViewController() 
        // this viewController is used when performing transitions with the Subcoordinator directly.
        super.init(viewController: viewController, parent: parent, map: nil)
    }
    
    /* ... */
    
    override func mapToParentRoute(for route: ChildRoute) -> ParentRoute {
        // you can map your ChildRoute enum to ParentRoute cases here that will get triggered on the parent router.
    }
}

🚏Using multiple coordinators with the same rootViewController

With XCoordinator 2.0, we introduce the option to use different coordinators with the same rootViewController. Since you can specify the rootViewController in the initializer of a new coordinator, you can specify an existing coordinator's rootViewController as in the following:

class FirstCoordinator: NavigationCoordinator<FirstRoute> {
    /* ... */
    
    override func prepareTransition(for route: FirstRoute) -> NavigationTransition {
        switch route {
        case .secondCoordinator:
            let secondCoordinator = SecondCoordinator(rootViewController: self.rootViewController)
            addChild(secondCoordinator)
            return .none() 
            // you could also trigger a specific initial route at this point, 
            // such as `.trigger(SecondRoute.initial, on: secondCoordinator)`
        }
    }
}

We suggest to not use initial routes in the initializers of sibling coordinators, but instead using the transition option in the FirstCoordinator instead.

⚠️ If you perform transitions involving a sibling coordinator directly (e.g. pushing a sibling coordinator without overriding its viewController property), your app will most likely crash.

πŸš€ RxSwift/Combine extensions

Reactive programming can be very useful to keep the state of view and model consistent in a MVVM architecture. Instead of relying on the completion handler of the trigger method available in any Router, you can also use our RxSwift-extension. In the example application, we use Actions (from the Action framework) to trigger routes on certain UI events - e.g. to trigger LoginRoute.home in LoginViewModel, when the login button is tapped.

class LoginViewModelImpl: LoginViewModel, LoginViewModelInput, LoginViewModelOutput {

    private let router: UnownedRouter<AppRoute>

    private lazy var loginAction = CocoaAction { [unowned self] in
        return self.router.rx.trigger(.home)
    }

    /* ... */
}

In addition to the above-mentioned approach, the reactive trigger extension can also be used to sequence different transitions by using the flatMap operator, as can be seen in the following:

let doneWithBothTransitions = 
    router.rx.trigger(.home)
        .flatMap { [unowned self] in self.router.rx.trigger(.news) }
        .map { true }
        .startWith(false)

When using XCoordinator with the Combine extensions, you can use router.publishers.trigger instead of router.rx.trigger.

πŸ“š Documentation & Example app

To get more information about XCoordinator, check out the documentation. Additionally, this repository serves as an example project using a MVVM architecture with XCoordinator.

For a MVC example app, have a look at some presentations we did about the Coordinator pattern and XCoordinator.

πŸ‘¨β€βœˆοΈ Why coordinators

  • Separation of responsibilities with the coordinator being the only component knowing anything related to the flow of your application.

  • Reusable Views and ViewModels because they do not contain any navigation logic.

  • Less coupling between components

  • Changeable navigation: Each coordinator is only responsible for one component and does not need to make assumptions about its parent. It can therefore be placed wherever we want to.

The Coordinator by Soroush Khanlou

⁉️ Why XCoordinator

  • Actual navigation code is already written and abstracted away.
  • Clear separation of concerns:
    • Coordinator: Coordinates routing of a set of routes.
    • Route: Describes navigation path.
    • Transition: Describe transition type and animation to new view.
  • Reuse coordinators, routers and transitions in different combinations.
  • Full support for custom transitions/animations.
  • Support for embedding child views / container views.
  • Generic BasicCoordinator classes suitable for many use cases and therefore less need to write your own coordinators.
  • Full support for your own coordinator classes conforming to our Coordinator protocol
    • You can also start with one of the following types to get a head start: NavigationCoordinator, ViewCoordinator, TabBarCoordinator and more.
  • Generic AnyRouter type erasure class encapsulates all types of coordinators and routers supporting the same set of routes. Therefore you can easily replace coordinators.
  • Use of enum for routes gives you autocompletion and type safety to perform only transition to routes supported by the coordinator.

πŸ”© Components

🎒 Route

Describes possible navigation paths within a flow, a collection of closely related scenes.

πŸ‘¨β€βœˆοΈ Coordinator / Router

An object loading views and creating viewModels based on triggered routes. A Coordinator creates and performs transitions to these scenes based on the data transferred via the route. In contrast to the coordinator, a router can be seen as an abstraction from that concept limited to triggering routes. Often, a Router is used to abstract from a specific coordinator in ViewModels.

When to use which Router abstraction

You can create different router abstractions using the unownedRouter, weakRouter or strongRouter properties of your Coordinator. You can decide between the following router abstractions of your coordinator:

  • StrongRouter holds a strong reference to the original coordinator. You can use this to hold child coordinators or to specify a certain router in the AppDelegate.
  • WeakRouter holds a weak reference to the original coordinator. You can use this to hold a coordinator in a viewController or viewModel. It can also be used to keep a reference to a sibling or parent coordinator.
  • UnownedRouter holds an unowned reference to the original coordinator. You can use this to hold a coordinator in a viewController or viewModel. It can also be used to keep a reference to a sibling or parent coordinator.

If you want to know more about the differences on how references can be held, have a look here.

πŸŒ— Transition

Transitions describe the navigation from one view to another. Transitions are available based on the type of the root view controller in use. Example: Whereas ViewTransition only supports basic transitions that every root view controller supports, NavigationTransition adds navigation controller specific transitions.

The available transition types include:

  • present presents a view controller on top of the view hierarchy - use presentOnRoot in case you want to present from the root view controller
  • embed embeds a view controller into a container view
  • dismiss dismisses the top most presented view controller - use dismissToRoot to call dismiss on the root view controller
  • none does nothing, may be used to ignore routes or for testing purposes
  • push pushes a view controller to the navigation stack (only in NavigationTransition)
  • pop pops the top view controller from the navigation stack (only in NavigationTransition)
  • popToRoot pops all the view controllers on the navigation stack except the root view controller (only in NavigationTransition)

XCoordinator additionally supports common transitions for UITabBarController, UISplitViewController and UIPageViewController root view controllers.

πŸ›  Installation

CocoaPods

To integrate XCoordinator into your Xcode project using CocoaPods, add this to your Podfile:

pod 'XCoordinator', '~> 2.0'

To use the RxSwift extensions, add this to your Podfile:

pod 'XCoordinator/RxSwift', '~> 2.0'

To use the Combine extensions, add this to your Podfile:

pod 'XCoordinator/Combine', '~> 2.0'

Carthage

To integrate XCoordinator into your Xcode project using Carthage, add this to your Cartfile:

github "quickbirdstudios/XCoordinator" ~> 2.0

Then run carthage update.

If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained over at Carthage.

Swift Package Manager

See this WWDC presentation about more information how to adopt Swift packages in your app.

Specify https://github.com/quickbirdstudios/XCoordinator.git as the XCoordinator package link. You can then decide between three different frameworks, i.e. XCoordinator, XCoordinatorRx and XCoordinatorCombine. While XCoordinator contains the main framework, you can choose XCoordinatorRx or XCoordinatorCombine to get RxSwift or Combine extensions as well.

Manually

If you prefer not to use any of the dependency managers, you can integrate XCoordinator into your project manually, by downloading the source code and placing the files on your project directory.

πŸ‘€ Author

This framework is created with ❀️ by QuickBird Studios.

To get more information on XCoordinator check out our blog post.

❀️ Contributing

Open an issue if you need help, if you found a bug, or if you want to discuss a feature request. If you feel like having a chat about XCoordinator with the developers and other users, join our Slack Workspace.

Open a PR if you want to make changes to XCoordinator.

πŸ“ƒ License

XCoordinator is released under an MIT license. See License.md for more information.

xcoordinator's People

Contributors

adammak avatar av0c0der avatar d2vydov avatar grafele avatar idevid avatar jdisho avatar jordanekay avatar maltebucksch avatar naeemshaikh90 avatar nasirky avatar pauljohanneskraft avatar qwite avatar ynnadrules avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

xcoordinator's Issues

How do you manage dependencies when testing?

Hello,

There is something I don't know exactly how to do.

My view models have a list of dependencies in the initialiser, same for the enums in the route.
So, I have something like:

enum BudgetRoute: Route {
    case budgetController(service: BudgetServiceProtocol, budgetDefaultOn: Bool, delegate: BudgetStatusDelegate)
    case tariffAlertController(service: TariffServiceProtocol, delegate: TariffAlertDelegate)
    case tariffController(service: TariffServiceProtocol)
    case dismiss
}

And the coordinator is:

final class BudgetCoordinator: NavigationCoordinator<BudgetRoute> {
    override func prepareTransition(for route: BudgetRoute) -> NavigationTransition {
        switch route {
        case let .budgetController(service, budgetDefaultOn, delegate):
            var vc = BudgetViewController.instantiateController()
            let viewModel = BudgetViewModel(coordinator: anyRouter,
                                            service: service,
                                            budgetDefaultOn: budgetDefaultOn,
                                            delegate: delegate)
            vc.bind(to: viewModel)
            return .push(vc)
        case let .tariffAlertController(service, delegate):
            var vc = TariffAlertController.instantiateController()
            let viewModel = TariffAlertViewModel(coordinator: anyRouter,
                                                 service: service,
                                                 delegate: delegate)
            vc.bind(to: viewModel)
            return .push(vc)
        case let .tariffController(service):
            let homeCoordinator = HomeCoordinator(initialRoute: .tariffController(service: service))
            let coordinator = AnyRouter<HomeRoute>(homeCoordinator)

            var vc = TariffController.instantiateController()
            let viewModel = TariffViewModel(coordinator: coordinator, service: service)
            vc.bind(to: viewModel)
            return .push(vc)
        case .dismiss:
            return .dismiss()
        }
    }
}

You can see how I pass my dependencies.
The problem I have is that sometimes the view mode calls something like:

                    self.coordinator.trigger(.budgetController(service: BudgetService(),
                                                               budgetDefaultOn: false,
                                                               delegate: self.budgetStatusDelegate))

Which means that I cannot test that scenario properly, because I cannot pass mocks.
Check "service: BudgetService()", that is not a mock, but a real service.

How should I manage that?
Should I pass to my view model all dependencies? I mean, even the ones that are going to be called when opening other view models when calling trigger (XCoordinator)?

Thanks a lot for suggestions.

Memory leak when pushing new coordinator to navigation coordinator

How to replicate

In your example app I have made this change in AppCoordinator.swift to make it easier to replicate...

        case .home:
            guard let presentable: Presentable = [
//                    HomeTabCoordinator(),
                    HomeSplitCoordinator(),
//                    HomePageCoordinator()
                ].randomElement() else {
                    return .none()
            }
            self.home = presentable
            return .present(presentable, animation: .staticFade)

Run this on an iPhone simulator in portrait mode (I have not tested in landscape but I imagine it will be the same.

Now press Login, <Home, Logout (repeat several times).

Open the memory graph debugger

You will see that there are now multiple instances of NewCoordinator (and others).

screenshot 2019-01-09 at 11 59 54

Cause of leak

The transition to the news detail is pushing a Coordinator (C) which then dispenses a UIViewController (V). This is then on a navigation stack. So has a back button.

When the back button is tapped the view controller V is then popped from the navigation stack. But the coordinator C is still held in memory. Something along those lines anyway. I haven't had enough time to fully track it down at the moment but this should be enough to go by for now.

How can I push without animation?

Hello,

How can I push a view controller inside a navigation controller without any animation? Just present it immediately.

I tried
return .push(vc, animation: nil)
and
let noAnimation = Animation(presentationAnimation: nil, dismissalAnimation: nil)
return .push(vc, animation: noAnimation)
but it does not work.

Any suggestion is welcome.

Registering Interactive Transition for RedirectionCoordinator

Hey. In my case I have NavigationCoordinator on which I push RedirectionCoordinator.

From this place I would like to apply a gesture driven interactive transition in order to pop back to main super coordinator.

Unfortunately registerInteractiveTransition method from Example project is just an extension of BaseCoordinator and I got an error:

Use of unresolved identifier 'registerInteractiveTransition'

So how can I achieve custom gesture driven pop interaction transition from RedirectionCoordinator?

Also I would like to know how to enable back/left swipe gesture for elements of this Navigation Controller stack. Is it possible?

Communicate a coordinator with another one

Hello,

Let's say I have an AppCoordinator, that contains coordinator First and coordinator Second.

AppCoordinator shows a view controller in First coordinator.
User presses a button (like Login)
Then AppCoordinator receives a signal and dismisses that view controller (that was presented modally) and releases First coordinator, after that, instantiates Second coordinator presenting another view controller (again, modally).

Basically, I want that when pressing that button, something happens not in that vc/vm coordinator, but in the parent coordinator.

I am not sure if this is exactly triggering a route.

Should I use a notification and put that logic there, inside the AppCoordinator, in a method called by a notification? Something like:
https://github.com/quickbirdstudios/XCoordinator/blob/master/Example/XCoordinator/Coordinators/AppCoordinator.swift#L59

BTW, in your sample code I see that after you login, that view controller stays alive, it is not released. That is exactly what I don't want.

Any suggestion about how to achieve this?

Thanks a lot.

How to chaining coordinators

Hey how is it to make route from a parent coordinator to child coordinator’s route?

Can not seem to find in doc
-Thanks!

How do you manage a screen that can be opened anywhere?

Hello,
I would like to know your opinion about this.
Right now my app has several routes, like SettingsRoute, DashboardRoute, Walkthrough...
However, if the network API is obsoleted, I would like to present a modal screen with just one button that brings the user to the app store, so he can update the app.
That screen blocks the app and this can happen at any point (it is a header in the API).
How should I manage this case?

Dismissal observable doesn't work properly for .push

Hello, thx for great framework! I bumped into one problem. When I use .push transition type dismissalObservable doesn't emit when I went back from navigation controller. I think that's because viewController.isBeingDismissed still false when viewController.rx.sentMessage(#selector(UIViewController.viewWillDisappear)) emits.

Link CocoaPods wrong

capture d ecran 2018-08-07 a 10 14 21

I have this error when i try to use with CocoaPods. And the link is not work.

Double dismiss

Hey, How to trigger double dismiss() transition when I have 2 navigation controllers (each as independent xcoordinator) presented on a vc? I need to trigger it from the one coordinator which is on the top of this stack ofc.

Btw awesome job guys. Very useful framework.

Optional linkage of RxSwift for Carthage

As stated in #74, optional linkage of RxSwift for XCoordinator would be beneficial for Carthage users as well. Since the PR did not solve the issue right away and further attention is required, this issue can be seen as a discussion/idea board and as a reminder.

Embed an UIViewController in an UIView

Hello,

I need some help to embed an UIViewController in an UIView. I have following screen separation:

A controller to present

----------------------
|  Placeholder View  | <- Here I want to embed a top bar
----------------------
|                    |
|         Main       |
|                    |
----------------------

A controller to embed

----------------------
|       Top Bar      |
----------------------

If I do something like

let viewController = MainViewController(...)
let topBar = TopBarViewController(...)
return .multiple(.push(viewController), .embed(topBar, in: viewController))

Then a whole view of UIViewController displays a top bar.

I can't specify to embed in a placeholder view, because this view is not initialized at the moment.

let viewController = MainViewController(...)
let topBar = TopBarViewController(...)
// viewController.placeholderView is nil at the moment
return .multiple(.push(viewController), .embed(topBar, in: viewController.placeholderView))

What can I do to embed TopBarViewController in a placeholder view?

PageCoordinator memory leak

Using PageCoordinator in your example I've noticed memory leaks.
The problem consist in PageTransition.swift that calls $0.presented(from: performer) only on the first page, and not all the pages of UIPageViewController.

Memory leak arrives from

private let rootViewControllerBox = ReferenceBox<RootViewController>()

which not releasing strong reference because is not called for every page

open func presented(from presentable: Presentable?) {
    rootViewControllerBox.releaseStrongReference()
}

Thanks

How to pass dependencies between modules?

I see that this library took as a basis code from the RxSwift book. To me, this approach also seemed very laconic and clean. I see the main difference between your approach and the book. In the book, the authors propose to transfer the object of the viewModel in the enum case, you just pass the necessary data (string, number, etc.). The only option I see. add one more associated property to case. What do you think?

Deep link question and infinite loop when trying to display TabNavigator

A change in c26da8c causes the example app to go into an infinite loop when displaying the TabCoordinator.

Animation+Navigation.swift in

public func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask

before:

?? navigationController.visibleViewController?.supportedInterfaceOrientations

after:

?? navigationController.parent?.supportedInterfaceOrientations

Not sure why the parent is called into here, other functions in this file also refer to the visibleViewController. You can reproduce the infinite loop by replacing HomePageCoordinator with HomeTabCoordinator in the AppCoordinator .home route handler.

––––––––––––––––––––––––––––

I'm also having some issues with deep linking. The first issue is that I cannot get it to work with a TabCoordinator for some reason, I get the Could not find appropriate router for... assertion when including a TabCoordinator route into the deepLink call.

Also it seems like the first route passed to the deepLink function is not used at all, whereas I would expect that route to be treated like all the others passed in. If you use the following implementation for AppCoordinator you will see what I mean:

class AppCoordinator: NavigationCoordinator<AppRoute> {

    init() {
        super.init(initialRoute: nil)
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
            self.trigger(.deep)
        }
    }

    var homeRouter: AnyRouter<HomeRoute>!

    override func prepareTransition(for route: AppRoute) -> NavigationTransition {
        switch route {
        case .login:
            var vc = LoginViewController.instantiateFromNib()
            let viewModel = LoginViewModelImpl(coordinator: anyRouter)
            vc.bind(to: viewModel)
            return .push(vc)
        case .home:
            self.homeRouter = HomeTabCoordinator().anyRouter
            let animation = Animation(presentationAnimation: StaticTransitionAnimation.flippingPresentation, dismissalAnimation: nil)
            return .present(homeRouter, animation: animation)
        case .deep:
            return deepLink(.login, AppRoute.home, HomeRoute.news, NewsRoute.newsDetail(MockNewsService().mockNews.first!))
        }
    }
}

Notable changes are that the initial route is now nil, so you start the app with an empty view. The deep route is triggered 3 seconds later, and you would think that you'd first see login but that route is not triggered, instead home is the first. This could be intended but is non obvious. I'm trying to integrate this lib with a pretty large and complicated app and I'm pushing the boundaries of what it's capable of and have lots of questions, I'd love to be able to chat with you in realtime if possible. Any plans to set up a Slack channel for this repo?

Thanks,
Andrew

Animations only work when presenting controller, not when pushing controller

I am trying to use a custom animation to push a controller, however the animation does not work when pushing. Only seems to work when presenting. Here is code sample where I have just use the flipping animation taken from the example project.

Here is an example of the code

 case .testController:
            var vc = TestController.instantiateController()
            let model = TestModel(coordinator: coordinator)
            
            let animation = Animation(presentationAnimation: CustomAnimations.flippingPresentation,
                                      dismissalAnimation: nil)
            
            vc.bind(to: model)
            return .push(vc, animation: animation)

It just seems to ignore the animation object and pushes the controller using standard navigation animation.

However, if I change the push to present

return .present(vc, animation: animation)

The animation works as expected.

Is this a bug with the framework? Or am I doing something wrong?

Any help would be much appreciated

Edit: Even in the demo project, it seems the pushing animation example does not work. In the HomeRoute class, there is this example

case .users:
           let animation = Animation(presentationAnimation: CustomPresentations.flippingPresentation, dismissalAnimation: nil)
           var vc = UsersViewController.instantiateFromNib()
           let viewModel = UsersViewModelImpl(coordinator: coordinator)
           vc.bind(to: viewModel)
           return .push(vc, animation: animation)

However it just pushes controller with standard navigation animation.

How implemented the PageCoordinator ?

I have the root coordinator, HomeTabCoordinator, and this router I set two tabbar items, AboutController and NewsController and created for them coordinate classes.

For NewsCoordinator I have next router:

enum NewsListRouter: Route {
    case newsList
    case selectedNews(newsID: Int)
}

and I open the NewsDetailViewController next way:

func detailNewsTrigger(_ id: Int) {
        router.trigger(.selectedNews(newsID: id))
    }

For this screen I have the next design, buttons for news paging:

Screenshot 2019-03-24 at 01 54 31

How do I implement the function, when tapped for NextButton, showing next news and also, the previewButton, showing preview news ?

I know about the PageCoordinator, but no idea how it implemented for my project, although looking at your example HomePageCoordinator. Please, help

-UIViewShowAlignmentRects YES will crash

I am using this excellent coordinator pattern in my project, every thing goes fine until I run project with "-UIViewShowAlignmentRects YES"

To reproduce the problem, just use the XCoordinator Example project and set Run Scheme->Arguments-> Arguments Passed On Launch->-UIViewShowAlignmentRects YES

According to the log:
23 XCoordinator 0x0000000102ebe883 $s12XCoordinator11PresentablePAAE7setRoot3forySo8UIWindowC_tF + 227

May be it has something to do with the way on how XCoordinator been set up in "didFinishLaunchingWithOptions"?

Registered Interactive Transition for dismiss

Hey, I'm trying to figure out how can I trigger interactive transition for dismiss route.
I found a good example how to implement it in your Examples folder.

    private func addPushGestureRecognizer(to container: Container) {
        let view = container.view
        let gestureRecognizer = UIScreenEdgePanGestureRecognizer()
        gestureRecognizer.edges = .right
        view?.addGestureRecognizer(gestureRecognizer)
        
        registerInteractiveTransition(
            for: .randomColor,
            triggeredBy: gestureRecognizer,
            progress: { [weak view] recognizer in
                let xTranslation = -recognizer.translation(in: view).x
                return max(min(xTranslation / UIScreen.main.bounds.width, 1), 0)
            },
            shouldFinish: { [weak view] recognizer in
                let xTranslation = -recognizer.translation(in: view).x
                let xVelocity = -recognizer.velocity(in: view).x
                return xTranslation >= UIScreen.main.bounds.width / 2
                    || xVelocity >= UIScreen.main.bounds.width / 2
            },
            completion: nil
        )
    }

But when we change route from .randomColor to .users which is a dismiss transition, animation happen immediately. I guess method name addPushGestureRecognizer is not called that by accident πŸ˜‰
How can we achieve interaction transition for dismiss?

Correct structure for PageCoordinator in TabBarCoordinator

Hi, First thanks for this great lib.

Unfortunately I wasnt able to figure out, how should I structure this kind of relationship, I mean home many coordinators, and routers should I create.

In My example I have Tab bar, in first tab there is view on top of screen, segmented control, under it should be container view and on segmented control change I should change View Controller in container view, I thought that it would be good to place page controller in container view, so I will change page controller's page.

I have created HomeCoordinator: TabBarCoordinator TabRoute, in tabRoute I wrote all tabs cases, my understanding is first coordinator of tab bar, should be simple view controller, which will have segmented control and container view with pager, is this correct?

Thanks, sorry for not so clear explanation

Dismissal animation transition gets dropped

A change was made in commit c26da8c on the transition-rework branch that introduces an issue where the dismissal animation passed into a present transition gets lost.

TransitionPerformer+Transition.swift
Before:

    func present(_ viewController: UIViewController, with options: TransitionOptions, animation: Animation?, completion: PresentationHandler?) {
        viewController.transitioningDelegate = animation
        rootViewController.present(viewController, animated: options.animated, completion: completion)
    }

After:

    func present(_ viewController: UIViewController, with options: TransitionOptions, animation: Animation?, completion: PresentationHandler?) {
        let previousTransitionDelegate = viewController.transitioningDelegate
        if let animation = animation {
            viewController.transitioningDelegate = animation
        }
        rootViewController.present(viewController, animated: options.animated) {
            viewController.transitioningDelegate = previousTransitionDelegate
            completion?()
        }
    }

the issue in the new version is viewController.transitioningDelegate = previousTransitionDelegate, where if previousTransitionDelegate is nil then the side effect is that when dismiss() is later called there is no transitioningDelegate set to handle the custom transition.

One thing to note is that I am presenting and dismissing from a NavigationCoordinator, not sure if that makes a difference. I tried to get a custom modal dismissal animation working in your sample app as well in the NewsCoordinator but am seeing the same thing, here's the change I made to try and reproduce:

    override func prepareTransition(for route: NewsRoute) -> NavigationTransition {
        switch route {
        case .news:
            var vc = NewsViewController.instantiateFromNib()
            let service = MockNewsService()
            let viewModel = NewsViewModelImpl(newsService: service, coordinator: anyRouter)
            vc.bind(to: viewModel)
            return .push(vc)
        case .newsDetail(let news):
            var vc = NewsDetailViewController.instantiateFromNib()
            let vm = NewsDetailViewModelImpl(news: news)
            vc.bind(to: vm)
            let animation = Animation(presentationAnimation: StaticTransitionAnimation.flippingPresentation,
                                      dismissalAnimation: StaticTransitionAnimation.flippingPresentation)
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
                self.trigger(.close)
            }
            return .present(vc, animation: animation)
        case .close:
            return .dismiss()
        }
    }

I also tried passing in an Animation to the .dismiss() with no luck.

In TransitionPerformer+Transition.swift the present function sets transitioningDelegate on the VC you are presenting, whereas in the dismiss function the transitioningDelegate is set on the rootViewController which in my case is UINavigationController created by the NavigationCoordinator. Passing a value in animation to dismiss() there has no effect, as I believe the actual VC being presented is the one that needs to have transitioningDelegate set.

Perhaps I'm going about this incorrectly, let me know what you think!

How best to handle routes up the coordinator chain?

Let's say I have a hierarchy of many Coordinators that are presented, and in the deepest level I then need to trigger a specific route defined at the root level which would do something like blow away the Coordinator hierarchy and all VCs for a different root route. Have you experimented with this use case?

One approach I'm considering is defining a single Route type for the whole app, and sticking every single possible route in that one enum AppRoute, have all custom coordinators use it, then optionally inject a parent of AnyCoordinator into each of my coordinators. When prepareTransition is called I'd implement only the routes that coordinator supports and in the .default case call `

parent?.trigger(route)
return .none()

Any other approaches I should consider?

I'm using the transition-rework branch too btw.

SplitCoordinator Initialization

If the window was not been appears and rootViewController of master is a UINavigationController, SplitCoordinator.init(master: Presentable, detail: Presentable?) works in an unexpected way.

For example:

import UIKit
import XCoordinator

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    let window: UIWindow! = UIWindow()
    let router = SomeCoordinator().anyRouter

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        router.setRoot(for: window!)
        return true
    }
}

enum SomeRoute: Route { }

class SomeCoordinator: SplitCoordinator<SomeRoute> {
    init() {
        let masterVC = UIViewController()
        masterVC.title = "Master"

        let detailVC = UIViewController()
        detailVC.title = "Detail"

        super.init(
            master: UINavigationController(rootViewController: masterVC),
            detail: detailVC
        )
    }
}

Untitled

I found the problem in BaseCoordinator.performTransitionAfterWindowAppeared(_ transition: TransitionType). If performTransition is wrapped asynchronously, then everything works as expected.

private func performTransitionAfterWindowAppeared(_ transition: TransitionType) {
    guard UIApplication.shared.keyWindow == nil else {
        return performTransition(transition, with: TransitionOptions(animated: false))
    }

    var windowAppearanceObserver: Any?

    rootViewController.beginAppearanceTransition(true, animated: false)
    windowAppearanceObserver = NotificationCenter.default.addObserver(
    forName: UIWindow.didBecomeKeyNotification, object: nil, queue: .main) { [weak self] _ in
        windowAppearanceObserver.map(NotificationCenter.default.removeObserver)
        windowAppearanceObserver = nil
        DispatchQueue.main.async {
            self?.performTransition(transition, with: TransitionOptions(animated: false))
        }
        self?.rootViewController.endAppearanceTransition()
    }
}

Transition between coordinators have problems with "whose view is not in the window hierarchy!"

Hi,

I'm trying to change initialRoute depend on business logic but I got a problem with the warning "whose view is not in the window hierarchy!"

The code blow taken from repo's example

/// I have change from .Login to .Home at AppDelegate
coordinator = BasicCoordinator<MainRoute>(initialRoute: .home, initialLoadingType: .immediately) 

let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = coordinator.rootViewController
window.makeKeyAndVisible()
self.window = window

Then I got the error
Warning: Attempt to present <UINavigationController: 0x7face0815400> on <UINavigationController: 0x7face1822e00> whose view is not in the window hierarchy!

Can some one tell me what cause this problems ? And how to fix this bug ?

How can I release previous screens?

Hello,

I have a problem and I don't really know how to fix it.

Let's say I have a route, OnBoardingRoute, which is a navigation controller with several screens.
Then the last screen, a modal screen is presented (animation from the bottom), called DashboardRoute (or HomeRoute). Using .present(vc)
Once you are there, there is no possibility to dismiss or go back, that is, now it is a completely independent screen.
Right now, I realised that all the screens presented previously are still on memory, which is normal, because it is a modal view.
How can I release them?

Thanks.

(Is this related with RedirectionRouter/RedirectionCoordinator?)

Multiple .embed(ed) view controllers

Hi,
Solid coordinator framework!

I've noticed I can use .embed as a navigation transition to implement child view controllers.
But what if I need to embed multiple child view controllers at once? Should I trigger multiple routes at once?

And also, what would be the proper way of dismissing or switching those embedded view controllers?

Thanks!

Support .push of NavigationCoordinator

Hello.

Currently a NavigationCoordinator can't push another NavigationCoordinator. If we try to do so, it results in an UIKit exception *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Pushing a navigation controller is not supported'.

I believe a Coordinator should allow being shown in multiple ways and that there's a use case for this functionality.

To make it work I think the base NavigationCoordinator (pusher) would just need to keep a reference to the topViewController of its UINavigationController before pushing the new NavigationCoordinator and the pushed NavigationCoordinator would have to use the pusher UINavigationController instead of creating one.
To rewind when a pop is made, the base NavigationCoordinator would just need to popToViewController(topViewController) and resume.

What are your thoughts on this?

Installation problem (Cocoapods)

When I call pod install in terminal I receive a red error:

pod install
Analyzing dependencies
[!] Unable to find a specification for `XCoordinator (~> 1.0)`

Previously I was using :branch => 'feat/v1' so maybe this makes the problem?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.