MonarchRouter版本1.1.2

MonarchRouter1.1.2

Eliah SnakinEliah Snakin维护。




  • nikans

MonarchRouter

Version License Platform

Monarch Router

一个轻量但强大的功能状态路由器,用Swift编写。

使用常见的URL约定进行路由。如果您曾经开发过服务器端应用路由,这将使您感到宾至如归。

Monarch Router 是一个声明式路由处理程序,它通过协调器和演示器将ViewControllers分离。它非常适合与Redux风格的状态流和响应式框架配合使用。

协调器是通过声明一个与URL结构相对应的路由层次结构构建的。演示者抽象了UI的创建和修改。

Monarch Router 通过 SPM 和 Cocoapods 分发。

帝王蝶重不到1克,但在迁徙过程中能覆盖数千英里。它被认为是一种标志性的授粉者和最美丽的蝴蝶种类之一。

特性

  • 在路径更改时导航复杂的ViewControlles层次结构并展开。
  • 根据URL约定解析和传递路由参数到端点。
  • 深链接处理推送通知、快捷方式和通用链接。
  • 导航分支(类似于标签页的演示器)。
  • 导航堆叠(例如导航控制器)。
  • 打开和关闭模态,具有自己的层次结构。
  • 通过更改窗口的根ViewController来切换顶级应用程序部分。
  • 场景处理。
  • 处理通用应用程序中的导航。(欢迎PR!)
  • 正确地抽象路由层以处理macOS应用程序中的导航。

术语表

  • Router:您的应用的路由协调器(具有子节点的根RoutingNode);或者更广泛地说,整个系统。

  • RoutingNode:收集与相同端点或中间路由点相关的函数的结构,具有子节点。每个RoutingNode还需要一个Presenter,任何所需更改都传递给它。

  • RoutePresenter:用于创建和配置Presentable(即UIViewController)的结构。有多种类型的演示者:端点、堆栈(用于导航树)、分歧(用于标签)、开关(用于解耦应用程序部分)。

  • Presentable:实际要显示的对象(即UIViewController)。

  • 懒惰演示者:一个围绕演示者创建函数的懒惰包装器,它包装演示者作用域,但仍未在调用之前创建Presentable

  • RoutingRequest:用于定义您想要导航到的端点的URL或类似于URL的结构。

  • Route:用于定义与RoutingRequest匹配规则的元素,以触发将路由到特定的RoutingNode

  • RouterStore:保留路由器的状态。提供了一种通过还原器调度RoutingRequest并修改状态的方法。

  • RouterState:保留活跃的RoutingNode的堆栈。

  • RouterReducer:是一个计算新状态的函数。通过RoutingNode的回调实现导航,取消未使用的RoutingNode

基本流程

  1. RouterStore上分发RouteRequest。请求是一个URL或类似于URL的结构。
  2. 通过还原器计算新状态,将请求与协调器层次结构匹配。层次结构中的每个节点都与一个Route(匹配规则)和一个抽象UI的Presenter相关联。
  3. 取消使用中的节点及其相应的演示者,根据计算出的状态重新创建新的演示者层次结构。

示例

示例项目说明了路由器的基本用法,以及一些非平凡用法,例如处理模态和深度链接。

如果您更喜欢使用Cocoapods而不是SPM,请从Example目录克隆存储库,然后首先从Example目录运行pod install

查看示例应用

如何使用

0. 从创建RouterStore开始。

在您的App或SceneDelegate中持久化它。

// Initializing Router and setting root VC
let coordinator = appCoordinator()
let router = RouterStore(router: coordinator)

self.appRouter = router

1. 定义您的App的Routes

Routes是用于与RoutingRequest进行匹配的规则。

/// Your app custom Routes
enum AppRoute: String, RouteType
{
    case login = "login"
    case today = "today"
    case story = "today/story/:type/:id"
    case books = "books"
    case book  = "books/:id"
}

一个route由RouteComponent组成。这些组件与RouteRequestPathComponent匹配(见下文)。

定义Route的方法有几种:

String符合RouteType

  • 组件使用/分隔。
  • 常量组件只是字符串(例如login)。
  • 参数组件以:为前缀。
  • 要为组件匹配任何内容,请使用:_
  • 要匹配路径的末尾的所有内容,请使用...

使用内置的RouteString结构来创建正则表达式验证的route。

typealias ParameterValidation = (name: String, pattern: String)
init(_ predicate: String, parametersValidation: [ParameterValidation]? = nil)
  • 使用上述规则设置谓词字符串。
  • 可选地添加一个ParameterValidation数组,其中name是参数名(不带:),pattern是一个正则表达式。

RouteComponent数组组成的项目符合RouteType

使用RouteComponent枚举来构建您的Route

enum RouteComponent 
{
    /// Matches a constant component
    case constant(String)
    
    /// Matches a parameter component
    /// - parameter name: parameter name to match
    /// - parameter isMatching: optional closure to match parameter value
    case parameter(name: String, isMatching: ((_ value: Any) -> Bool)? )
    
    /// Matches any path component for a route component
    case anything
    
    /// Matches any path to the end
    case everything
}

1.1. 可选地定义一组 RoutingRequest

RoutingRequest 与与 RoutingNode 相关的 Route 进行匹配。

为了方便起见,Monarch Router 使用 URL 或有效的类似 URL 的 String 触发路由。

可用的 URL 部分

  • 路径组件 (books/:id)
  • 查询项 (?name=eliah)
  • 片段 (#documentation)

您可以直接派发 URLString。或者您可以创建一个自定义枚举

enum AppRoutingRequest: RoutingRequestType
{
    case login
    case today
    case story(type: String, id: Int, title: String)
    case books
    case book(id: Int, title: String?)

    var request: String {
        switch self {
        case .login:
        return "login"

        case .today:
        return "today"

        case .story(let type, let id, let title):
        return "today/story/\(type)/\(id)?title=\(title)"

        case .books:
        return "books"

        case .book(let id, let title):
        return "books/\(id)?title=\(title ?? "")"
    }
}

如果您为了方便已经决定定义一个自定义的 RoutingRequestType 枚举,您需要一个解析函数。由于在这里我们正在将我们的请求映射到 String,我们将使用其内置解析器。

func resolve(for route: RouteType) -> RoutingResolvedRequestType {
    return request.resolve(for: route)
}

匹配的展示器可以使用解析的 RouteParameters 对象进行参数化(见下文)。

虽然您可以配置基于查询参数和片段的展示器,但只有路径参数用于匹配。

1.2. 在 RouterStore 对象上派发路由请求

router.dispatch(.login)

您可能希望将 RouterStore 实现隐藏在一个专门的 ProvideRouteDispatch 协议后面,即

protocol ProvidesRouteDispatch: class
{
    /// Extension method to change the Route.
    /// - parameter request: `AppRoutingRequest` to navigate to.
    func dispatch(_ request: AppRoutingRequest)
}

extension RouterStore: ProvidesRouteDispatch { 
    func dispatch(_ request: AppRoutingRequest) {
        dispatch(request.request)
    }
}

但首先我们需要创建一个协调器。

2. 创建应用程序的协调器

协调器是一个层级的 RoutingNode 结构。

/// Creating the app's Coordinator hierarchy.
func appCoordinator() -> RoutingNodeType
{    
    return
    
    // Top level app sections' switcher
    RoutingNode(sectionsSwitcherRoutePresenter()).switcher([

        // Login 
        // (section 0)
        RoutingNode(lazyPresenter(for: .login))
            .endpoint(AppRoute.login),

        // Tabbar 
        // (section 1)
        RoutingNode(lazyTabBarRoutePresenter()).fork([

                // Today nav stack
                // (tab 0)
                RoutingNode(lazyNavigationRoutePresenter()).stack([

                    // Today
                    RoutingNode(lazyPresenter(for: .today))
                        .endpoint(AppRoute.today, modals: [

                        // Story 
                        // (presented modally)
                        RoutingNode(lazyPresenter(for: .story))
                            .endpoint(AppRoute.story)
                    ])
                ]),

                // Books nav stack
                // (tab 1)
                RoutingNode(lazyNavigationRoutePresenter()).stack([

                    // Books
                    // (master)
                    RoutingNode(lazyPresenter(for: .books))
                        .endpoint(AppRoute.books, children: [

                        // Book
                        // (detail)
                        RoutingNode(lazyPresenter(for: .book))
                            .endpoint(AppRoute.book)
                        ])
                ])
            ])
    ])
}

根据其 Presenter,一个 RoutingNode 可以执行四种类型的行为之一

  • 端点
  • 堆栈(即导航树)
  • 分支(即标签页)
  • 切换器(解耦应用程序的部分)

每个 RoutingNode 都会将其 RoutingRequest 与其 Route 进行匹配(即 .endpoint(AppRoute.today)),或者与子路由进行匹配(非端点类型的节点)。然后选择合适的子层次结构,并将 RouterState 简化到新的一个。

然后新的节点堆栈的 Presenter 在必要时会实例化其 Presentable(即 UIViewControler),自动重建应用程序的导航层次结构。

UI 魔法在展示器中被抽象化。

3. 创建 演讲者

可展示配置

演讲者的主要目标是创建一个 可展示 对象。因此,当您定义一个 演讲者 时,您必须传递一个用于创建可展示的闭包:getPresentable: () -> (UIViewController)。目前,仅支持 UIViewController 的子类型。

如果使用一些 RouteParameter 调用了 演讲者,则会调用一个可选的闭包,允许配置 可展示setParameters: ((_ parameters: RouteParameters, _ presentable: UIViewController) -> ()

注意:使可展示符合 RouteParametrizedPresentable,以自动处理此功能。

当节点不再被选择时,将调用一个可选的闭包 unwind: (_ presentable: UIViewController) -> ()。如果您正在进行特殊处理,请设置它。

重要:每个 演讲者 都可以直接或延迟实例化。建议在协调器层次结构中使用延迟初始化,否则所有可展示对象都将应用于应用启动时。

内置演讲者

RoutePresenter

用于端点展示。

端点演讲者能够使用自己的层次结构展示和 dismissing 模态。相应的闭包会被调用。

/// Callback executed when a modal view is requested to be presented.
presentModal: (_ modal: UIViewController, _ over: UIViewController) -> ()

/// Callback executed when a presenter is required to close its modal.
dismissModal: ((_ modal: UIViewController)->())

模态展示是默认的,因此您可能只想使用它们来实现特殊行为。

RoutePresenterFork

用于标签栏样式的展示。

使用特殊的闭包来配置可展示对象(例如 UITabBarController

/// Sets the options for Router to choose from
setOptions: (_ options: [UIViewController], _ container: UIViewController) -> ()

/// Sets the specified option as currently selected.
setOptionSelected: (_ option: UIViewController, _ container: UIViewController) -> ()

在通过根路由切换到标签页时使用 .junctionsOnly 调度选项,如果标签页已经包含展示的堆栈。

RoutePresenterStack

用于组织导航堆栈中的其他展示器(例如,UINavigationController)。

/// Sets the navigation stack
setStack: (_ stack: [UIViewController], _ container: UIViewController) -> ()

/// Presets root Presentable when the stack's own Presentable is requested
prepareRootPresentable: (_ rootPresentable: UIViewController, _ container: UIViewController) -> ()
RoutePresenterSwitcher

用于在解耦的应用部分之间切换(例如,登录序列,主序列等)

/// Sets the specified option as currently selected.
setOptionSelected: (_ option: UIViewController) -> ()

此展示器可能没有展示器。

示例展示器

示例应用包含几个有用的展示器,不是作为库的一部分,比如:

  • UITabBarController 展示器基于 RoutePresenterFork 构建,带有一个委托来处理点击时的路由请求。
  • UINavigationController 展示器基于 RoutePresenterStack 构建,有相关的弹出/推送等行为。
  • 部分切换展示器基于 RoutePresenterSwitcher 构建,可以设置 窗口rootViewController

原理概念

UI是状态的表示

随着状态随时间变化,UI的状态表示也随之变化。对于任何状态值,UI必须是可预测和可重复的。

设备相关状态应与路由状态分开。

例如,在手机和平板上显示相同的状态可能会导致不同的用户界面。设备相关状态应保留在该设备上。OS X和iOS应用可以使用相同的状态和逻辑类以及交换路由来表示状态。

尚未完全实现。欢迎PR!

UI可以生成操作以更新状态中的节点栈

用户点击返回按钮很容易捕获并生成一个更新状态的操作,这会导致用户界面发生变化。但用户“滑动”返回视图就很难捕获了。它应该在完成时生成一个操作以更新状态。然后,如果当前用户界面已经匹配新状态,则不需要进行用户界面更改。

安装

Swift包管理器

使用Xcode UI:转到您的项目设置 -> Swift Packages,然后在那里添加 [email 保护的邮件地址。

要使用Apple的Swift包管理器集成,不带Xcode集成,请将以下内容作为依赖项添加到您的Package.swift中

.package(url: "[email protected]:nikans/MonarchRouter.git", .upToNextMajor(from: "1.1.0"))

CocoaPods

要安装它,只需将以下行添加到您的 Podfile 中

pod 'MonarchRouter', '~> 1.1'

您可以在这里找到最后一个发布版本。

需求

当前仅支持 iOS/iPhone 8.0 及以上版本,但从理论上讲,它很容易扩展以支持通用应用程序。支持 macOS 需要使用泛型等抽象层的新层,我认为现在直接使用它会更清晰。不过,您也可以积极贡献!

  • iOS/iPhone
  • iOS/通用
  • macOS

作者

Eliah Snakin: [email protected]

Monarch Router 是从 Featherweight Router 的晶体中诞生的。

许可证

MonarchRouter 可在 MIT 许可证下使用。有关更多信息,请参阅 LICENSE 文件。