Route URL schemes easily.
Crossroad is an URL router focused on handling Custom URL Schemes or Universal Links. Of cource, you can also use for Firebase Dynamic Link or other similar services.
Using this, you can route multiple URL schemes and fetch arguments and parameters easily.
This library is developed in working time for Cookpad.
Basic Usage
You can use DefaultRouter
to define route definitions.
Imagine to implement Pokédex on iOS. You can access somewhere via 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")!)
Using AppDelegate
In common use case, you should call router.openIfPossible
on UIApplicationDelegate
method.
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
}
Using SceneDelegate
Or, if you are using SceneDelegate
with a modern app:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let context = URLContexts.first else {
return
}
router.openIfPossible(context.url, options: context.options)
}
Using NSApplicationDelegate (for macOS)
If you develop macOS applications:
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: [:])
}
}
Argument and Parameter
Argument
:
prefixed components on passed URL pattern mean argument.
For example, if passed URL matches pokedex://search/:keyword
, you can get keyword
from Context
.
// actual URL: pokedex://search/Pikachu
let keyword: String = try context.arguments(named: "keyword") // Pikachu
QueryParameter
And more, you can get query parameters if exist.
// 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
You can cast arguments/query parameters as any type. Crossroad attempt to cast each String values to the type.
// expected pattern: pokedex://search/:pokedexID
// actual URL: pokedex://search/25
let pokedexID: Int = try context.arguments(named: "keyword") // 25
Currently supported types are Int
, Int64
, Float
, Double
, Bool
, String
and URL
.
Enum arguments
You can use enums as arguments by conforming to 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
Comma-separated list
You can treat comma-separated query strings as Array
or Set
.
// matches: pokedex://pokemons?types=water,grass
let types: [Type]? = context.queryParameters.types // [.water, .grass]
Custom argument
You can also define own arguments by implementing Parsable
.
This is an example to parse custom struct.
struct User {
let name: String
}
extension User: Parsable {
init?(from string: String) {
self.name = string
}
}
Multiple link sources support
You can define complex routing definitions like following:
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()
}
}
This router can treat three link sources.
Custom Router
You can add any payload to 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)
Parse URL patterns
If you maintain a complex application and you want to use independent URL pattern parsers without Router.
You can use ContextParser
.
let parser = ContextParser()
let context = parser.parse(URL(string: "pokedex:/pokemons/25")!,
with: "pokedex://pokemons/:id")
Installation
Swift Package Manager
- File > Swift Packages > Add Package Dependency
- Add https://github.com/giginet/Crossroad.git Select "Up to Next Major" with "4.0.0"
CocoaPods
use_frameworks!
pod 'Crossroad'
Carthage
github "giginet/Crossroad"
Demo
- Open
Demo/Demo.xcodeproj
on Xcode. - Build
Demo
schema.
Supported version
Latest version of Crossroad requires Swift 5.2 or above.
Use 1.x instead on Swift 4.1 or lower.
Crossroad Version | Swift Version | Xcode Version |
---|---|---|
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 |
License
Crossroad is released under the MIT License.
Header logo is released under the CC BY 4.0 license. Original design by @Arslanshn.