交叉路程 4.2.0

交叉路程 4.2.0

giginet 维护。



Crossroad

Build Status Language SwiftPM compatible Carthage compatible CocoaPods Compatible Platform License

轻松实现路由 URL 方案。

交叉路程是一个专注于处理自定义 URL 方案或通用链接的 URL 路由器。当然,您还可以用于 Firebase Dynamic Link 或其他类似服务。

使用此方法,您可以轻松路由多个 URL 方案并检索参数。

这个库是在 Cookpad 的工作时间内开发的。

基本用法

您可以使用 DefaultRouter 来定义路由定义。

想象在 iOS 上实现 Pokédex。您可以通过 URL 方案访问某个地方。

import Crossroad

let customURLScheme: LinkSource = .customURLScheme("pokedex")
let universalLink: LinkSource = .universalLink(URL(string: "https://my-awesome-pokedex.com")!)

do {
    let router = try DefaultRouter(accepting: [customURLScheme, universalLink]) { registry in
        registry.route("/pokemons/:pokedexID") { context in 
            let pokedexID: Int = try context.argument(named: "pokedexID") // Parse 'pokedexID' from URL
            if !Pokedex.isExist(pokedexID) { // Find the Pokémon by ID
                throw PokedexError.pokemonIsNotExist(pokedexID) // If Pokémon is not found. Try next route definition.
            }
            presentPokedexDetailViewController(of: pokedexID)
        }
        registry.route("/pokemons") { context in 
            let type: Type? = context.queryParameters.type // If URL contains &type=fire, you can get Fire type.
            presentPokedexListViewController(for: type)
        }

        // ...
    }
} catch {
    // If route definitions have some problems, routers fail initialization and raise reasons.
    fatalError(error.localizedDescription)
}

// Pikachu(No. 25) is exist! so you can open Pikachu's page.
let canRespond25 = router.responds(to: URL(string: "pokedex://pokemons/25")!) // true
// No. 9999 is missing. so you can't open this page.
let canRespond9999 = router.responds(to: URL(string: "pokedex://pokemons/9999")!) // false
// You can also open the pages via universal links.
let canRespondUniversalLink = router.responds(to: URL(string: "https://my-awesome-pokedex.com/pokemons/25")!) // true

// Open Pikachu page
router.openIfPossible(URL(string: "pokedex://pokemons/25")!)
// Open list of fire Pokémons page
router.openIfPossible(URL(string: "pokedex://pokemons?type=fire")!)

使用 AppDelegate

在常规用例中,您应该在 UIApplicationDelegate 方法上调用 router.openIfPossible

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
    if router.responds(to: url, options: options) {
        return router.openIfPossible(url, options: options)
    }
    return false
}

使用 SceneDelegate

或者,如果您使用现代应用程序的 SceneDelegate,那么

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let context = URLContexts.first else {
        return
    }
    router.openIfPossible(context.url, options: context.options)
}

使用 NSApplicationDelegate (用于 macOS)

如果您正在开发 macOS 应用程序

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let appleEventManager = NSAppleEventManager.shared()
        appleEventManager.setEventHandler(self,
                                          andSelector: #selector(handleURLEvent(event:replyEvent:)),
                                          forEventClass: AEEventClass(kInternetEventClass),
                                          andEventID: AEEventID(kAEGetURL))
    }

    @objc func handleURLEvent(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) {
        guard let urlString = event?.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }
        guard let url = URL(string: urlString) else { return }
        router.openIfPossible(context.url, options: [:])
    }
}

参数和参数名

参数

: 前缀的组件在传入的 URL 模式上意味着 参数名

例如,如果传入的 URL 与 pokedex://search/:keyword 匹配,您可以从 Context 中获取 keyword

// actual URL: pokedex://search/Pikachu
let keyword: String = try context.arguments(named: "keyword") // Pikachu

查询参数

此外,如果存在,您还可以获取查询参数。

// actual URL: pokedex://search/Pikachu?generation=1
let generation: Int? = context.queryParameters["generation"] // 1
// or you can also get value using DynamicMemberLookup
let generation: Int? = context.queryParameters.generation // 1

您可以请 arguments/query parameters 转换为任何类型。Crossroad 尝试将每个 String 值转换为类型。

// expected pattern: pokedex://search/:pokedexID
// actual URL: pokedex://search/25
let pokedexID: Int = try context.arguments(named: "keyword") // 25

目前支持的类型有 IntInt64FloatDoubleBoolStringURL

枚举参数

您可以使用枚举作为参数,前提是符合 Parsable

enum Type: String, Parsable {
    case normal
    case fire
    case water
    case grass
    // ....
}

// matches: pokedex://pokemons?type=fire
let type: Type? = context.queryParameters.type // .fire

逗号分隔列表

您可以将逗号分隔的查询字符串视为 ArraySet

// matches: pokedex://pokemons?types=water,grass
let types: [Type]? = context.queryParameters.types // [.water, .grass]

自定义参数

您还可以通过实现 Parsable 来定义自己的参数。以下是一个解析自定义结构的示例。

struct User {
    let name: String
}
extension User: Parsable {
    init?(from string: String) {
        self.name = string
    }
}

支持多个链路源

您可以定义如下的复杂路由定义:

let customURLScheme: LinkSource = .customURLScheme("pokedex")
let pokedexWeb: LinkSource = .universalLink(URL(string: "https://my-awesome-pokedex.com")!)
let anotherWeb: LinkSource = .universalLink(URL(string: "https://kanto.my-awesome-pokedex.com")!)

let router = try DefaultRouter(accepting: [customURLScheme, pokedexWeb, anotherWeb]) { registry in
    // Pokémon detail pages can be opened from all sources.
    registry.route("/pokemons/:pokedexID") { context in 
        let pokedexID: Int = try context.argument(named: "pokedexID") // Parse 'pokedexID' from URL
        if !Pokedex.isExist(pokedexID) { // Find the Pokémon by ID
            throw PokedexError.pokemonIsNotExist(pokedexID)
        }
        presentPokedexDetailViewController(of: pokedexID)
    }

    // Move related pages can be opened only from Custom URL Schemes
    registry.group(accepting: [customURLScheme]) { group in
        group.route("/moves/:move_name") { context in 
            let moveName: String = try context.argument(named: "move_name")
            presentMoveViewController(for: moveName)
        }
        group.route("/pokemons/:pokedexID/move") { context in 
            let pokedexID: Int = try context.argument(named: "pokedexID")
            presentPokemonMoveViewController(for: pokedexID)
        }
    }

    // You can pass acceptPolicy for a specific page.
    registry.route("/regions", accepting: .only(for: pokedexWeb)) { context in 
        presentRegionListViewController()
    }
}

此路由器可以处理三个链路源。

自定义路由器

您可以向Router添加任何有效负载。

struct UserInfo {
    let userID: Int64
}
let router = try Router<UserInfo>(accepting: customURLScheme) { registry in
    registry.route("pokedex://pokemons") { context in 
        let userInfo: UserInfo = context.userInfo
        let userID = userInfo.userID
    }
    // ...
])
let userInfo = UserInfo(userID: User.current.id)
router.openIfPossible(url, userInfo: userInfo)

解析URL模式

如果您维护一个复杂的应用程序并且您想要使用独立的URL模式解析器而不使用路由器。您可以使用ContextParser

let parser = ContextParser()
let context = parser.parse(URL(string: "pokedex:/pokemons/25")!, 
                           with: "pokedex://pokemons/:id")

安装

Swift包管理器

CocoaPods

use_frameworks!

pod 'Crossroad'

Carthage

github "giginet/Crossroad"

演示

  1. 在 Xcode 中打开 Demo/Demo.xcodeproj
  2. 编译 Demo 架构。

支持的版本

Crossroad的最新版本需要 Swift 5.2 或更高。

在 Swift 4.1 及以下版本中使用 1.x。

Crossroad 版本 Swift 版本 Xcode 版本
4.x 5.4 Xcode 13.0
3.x 5.0 Xcode 10.3
2.x 5.0 Xcode 10.2
1.x 4.0 ~ 4.2 ~ Xcode 10.1

许可证

Crossroad 采用 MIT 许可证发布。

页眉徽标采用 CC BY 4.0 许可。由 @Arslanshn 设计。