轻松路由 URL scheme。
Crossroad 是一个 URL 路由器,专注于处理自定义 URL Schemes 或 Universal Links。 当然,你也可以将其用于 Firebase Dynamic Link 或其他类似服务。
使用它,你可以轻松地路由多个 URL scheme 并获取参数。
这个库是在 Cookpad 的工作时间内开发的。
你可以使用 DefaultRouter
来定义路由定义。
想象一下在 iOS 上实现一个 Pokédex (宝可梦图鉴)。 你可以通过 URL scheme 访问某些地方。
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")!)
在常见的用例中,你应该在 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
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let context = URLContexts.first else {
return
}
router.openIfPossible(context.url, options: context.options)
}
如果你开发 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
你可以将参数/查询参数强制转换为任何类型。 Crossroad 尝试将每个 String 值强制转换为该类型。
// expected pattern: pokedex://search/:pokedexID
// actual URL: pokedex://search/25
let pokedexID: Int = try context.arguments(named: "keyword") // 25
目前支持的类型是 Int
、Int64
、Float
、Double
、Bool
、String
和 URL
。
你可以通过遵循 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
你可以将逗号分隔的查询字符串视为 Array
或 Set
。
// 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
添加任何 payload。
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)
如果你维护一个复杂的应用程序,并且希望在没有 Router 的情况下使用独立的 URL 模式解析器。 你可以使用 ContextParser
。
let parser = ContextParser()
let context = parser.parse(URL(string: "pokedex:/pokemons/25")!,
with: "pokedex://pokemons/:id")
use_frameworks!
pod 'Crossroad'
github "giginet/Crossroad"
Demo/Demo.xcodeproj
。Demo
schema。最新版本的 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 许可证下发布。
标题 logo 在 CC BY 4.0 许可证下发布。 原始设计由 @Arslanshn 提供。