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.
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")!)
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
}
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)
}
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: [:])
}
}
:
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
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
.
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
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]
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
}
}
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.
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)
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")
- File > Swift Packages > Add Package Dependency
- Add https://github.com/giginet/Crossroad.git Select "Up to Next Major" with "4.0.0"
use_frameworks!
pod 'Crossroad'
github "giginet/Crossroad"
- Open
Demo/Demo.xcodeproj
on Xcode. - Build
Demo
schema.
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 |
Crossroad is released under the MIT License.
Header logo is released under the CC BY 4.0 license. Original design by @Arslanshn.