Swift for UIKit 中协调器模板的简单、强大和优雅的实现


安装

Swift Package Manager

https://github.com/bartleby/Jumper.git

如何使用跳线

协调 员

开箱即用的跳线具有三种协调器协议,您的协调器可以实现这些协议:

你也可以实现自己的协调器,参见源代码。

  • RootCoordinable– 它是一个容器,非常适合在应用程序中启动逻辑。UIViewController
  • NavigationCoordinable– 用于堆栈导航
  • TabCoordinable- 实现“UITabBarController”

根可协调

final class AppCoordinator: RootCoordinable {

    // step 1
    var navigation: RootNavigation<AppCoordinator> = .init(initial: \.onboarding)
    
    // step 2
    @Route var onboarding = onboardingScreen
    @Route var home = homeScreen

    // step 3
    func onboardingScreen() -> ScreenView {
        OnboardingScreenView()
    }
    
    func homeScreen() -> TabBarCoordinator {
        TabBarCoordinator()
    }
}

In this example, an is created that implements the protocolAppCoordinatorRootCoordinable

  • Step 1 – You have to implement the property and initialize it with by defaultnavigationRoute
  • Step 2 – Initialization of transitions, set using the keyword which should indicate the function of creating a or another for the transition@RouteViewCoordinator
  • Step 3 – Implementation of methods referenced by Route

List of transition methods

  • root(\.someRoute) Replaces the current view or coordinator
  • root(\.someRoute, input: "any type")
  • isRoot(\.someRoute) Returns a boolean value that indicates whether the given is rootRoute
  • hasRoot(\.someRoute) Returns the root coordinator or nil if the specified is not rootRoute
  • present(\.someRoute) Is presented by a view or coordinator
  • present(\.someRoute, input: "any type")
  • present(\.someRoute, input: "any type", animated: false)
  • dismiss() Dismiss the current Coordinator

You can pass an argument to each transition method using the field, which will be passed to the view/coordinator creation function.input

@Route var userList = userListScreen

func userListScreen(listData: [User]) -> UserListCoordinator {
    UserListCoordinator(data: listData)
}

calling such a transition will look like this

coordinator.present(\.userList, input: userListData)

NavigationCoordinable

final class AuthorizationCoordinator: NavigationCoordinable {
    
    var navigation: Navigation<AuthorizationCoordinator> = .init(initial: \.authorization)
    
    @Route var authorization = authorizationScreen
    @Route var registration = registrationScreen
    
    func authorizationScreen() -> ScreenView {
        AuthorizationScreen()
    }
    
    func registrationScreen() -> ScreenView {
        RegistrationScreen()
    }
}

List of transition methods

  • push(\.someRoute)
  • push(\.someRoute, input: "any type")
  • push(\.someRoute, animated: false)
  • push(stack: )
  • pop(\.someRoute)
  • pop(\.someRoute, animated: false)
  • pop(to: \.someRoute) going to the specified Route
  • pop(to: \.someRoute, animated: false)
  • popToRoot()
  • popToRoot(animated: false)
  • present(\.someRoute)
  • present(\.someRoute, input: "any type")
  • present(\.someRoute, input: "any type", animated: false)
  • dismiss() Dismiss the current Coordinator

using the method you can several into the navigation stack at once, and the animation will be applied only for the last transitionpush(stack:)pushRoutes

coordinator.push {
    \SettingsCoordinator.yellow
    \SettingsCoordinator.green
    \SettingsCoordinator.green
    \SettingsCoordinator.green
    \SettingsCoordinator.yellow
    \SettingsCoordinator.green
}

you can do the same with the chainRoute

coordinator
    .push(\.yellow, animated: false)
    .push(\.green, animated: false)
    .push(\.green, animated: false)
    .push(\.green, animated: false)
    .push(\.yellow, animated: false)
    .push(\.green, animated: true)

TabCoordinable

final class TabBarCoordinator: TabCoordinable {
    
    // Step 1
    var navigation: TabNavigation<TabBarCoordinator> = .init {
        \TabBarCoordinator.main
        \TabBarCoordinator.settings
    }
    
    // Step 2
    @Route(tabItem: mainTab) var main = mainScreen
    @Route(tabItem: settingsTab) var settings = settingsScreen

    // Step 3
    func mainScreen() -> MainCoordinator {
        MainCoordinator()
    }
    
    func settingsScreen() -> SettingsCoordinator {
        SettingsCoordinator()
    }
    
    // Step 4
    func mainTab() -> UITabBarItem {
        UITabBarItem()
            .image(UIImage(systemName: "circle.fill"))
            .title("Main")
    }
    
    func settingsTab() -> UITabBarItem {
        UITabBarItem()
            .image(UIImage(systemName: "square.fill"))
            .title("Settings")
    }
}

Here everything is similar to other coordinators, with a small exception, a new argument appears in @Route, to which you must pass TabItem, and in the initialization of the property, a list of routes that will be tabs is now passednavigation

  • Step 1 – Initialize with several that will be tabs in the tabbarnavigationRoutes
  • Step 2 – Define by specifying TabItem in the argumentRoute
  • Step 3 – Define the coordinators for transitions
  • Step 4 – Define the methods that will return TabItem’s

List of transition methods

  • focus(\.someTabRoute) switching to tab
  • present(\.someRoute) presentation of Route
  • dismiss() Dismiss the current Coordinator

Alert’s

In order to show the alert and other pop-up elements through the coordinator, you must support the protocolScreenViewPresentable

For example, create the protocol and implement the Alert display logic in it. To get the controller to which you want to show the popup element, call the methodAlertCoordinableview()

let controller = view()

public protocol AlertCoordinable: ScreenViewPresentable {
    func showAlert(title: String, message: String)
}

extension AlertCoordinable {
    func presentAlert(title: String, message: String) {
        let controller = view()
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let completeAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
        alertController.addAction(completeAction)
        controller.present(alertController, animated: true, completion: nil)
    }
}

Implement the ‘AlertCoordinable` protocol in the coordinator. Now the coordinator has the opportunity to display an alert

final class SettingsCoordinator: NavigationCoordinable, AlertCoordinable {
    //...
}
coordinator.presentAlert(title: "Title", message: "Message")

Customizing

Sometimes you may need to access the , or controller from your coordinator, there is a method for thisUITabBarControllerUINavigationControllerUIViewControllerconfigure(controller: )

final class MainCoordinator: NavigationCoordinable {
    
    //...
    
    func configure(controller: UINavigationController) {
        // Customize here
    }
}

Chaining

One of the strengths of is the integration of transitions into chainsJumper

coordinator
    .hasRoot(\.tabBar)
    .focus(\.todoList)
    .push(\.todoDetail, input: todoIdentifier)
    .present(\.todoEditor)

each transition, if it is a transition to the coordinator, returns the transition coordinator, if it is a transition to the view, then the current coordinator is returned.

For example: There are two transitions in the coordinator:SettingsCoordinator

final class SettingsCoordinator: NavigationCoordinable {
    
    //...
    
    @Route var rateApp = rateAppScreen
    @Route var notification = notificationScreen

    func rateAppScreen() -> ScreenView {
        RateAppViewController()
    }
    
    func notificationScreen() -> NotificationCoordinator {
        NotificationCoordinator()
    }
}

coordinator.present(\.rateApp) \\ return SettingsCoordinator
coordinator.present(\.notification) \\ return NotificationCoordinator

Deep Linking

By combining into chains, you get out of the box, to implement them, define the method in RouteDeepLink scene(scene:, openURLContexts:)SceneDelegate

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    appCoordinator.onOpenURL(URLContexts.first?.url)
}

define the method in your app coordinatoronOpenURL(url:)

func onOpenURL(_ url: URL?) {
    guard let url = url else { return }
    guard let deepLink = try? DeepLink(url: url) else { return }

    if let coordinator = self.hasRoot(\.home) {
        switch deepLink {
        case .todo(let id):
            coordinator
                .focus(\.main)
                .present(\.todo, input: id)
        case .settings:
            coordinator.focus(\.settings)
        case .home:
            coordinator.focus(\.main)
        }
    }
}

You can see the implementation of in the Demo project.DeepLink

To test the ‘DeepLink ‘ use the terminal command xcrun simctl openurl booted <url>

deep links that are configured in the Demo project

Switching to Main Tab

xcrun simctl openurl booted jumper://io.idevs/home

Switching to Settings Tab

xcrun simctl openurl booted jumper://io.idevs/settings

Opens the modal view and passes the argument to ithello-world

xcrun simctl openurl booted jumper://io.idevs/todo/hello-world

ezgif-4-59b7108f97

Demo project

Download the demo project from repository

License

MIT license. See the LICENSE file for details.

GitHub

点击跳转