Crossroad
  • September 13, 2023

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 IntInt64FloatDoubleBoolString 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

CocoaPods

use_frameworks!

pod ‘Crossroad’

Carthage

github “giginet/Crossroad”

Demo


  1. Open Demo/Demo.xcodeproj on Xcode.
  2. 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

GitHub


View Github

YOU MIGHT ALSO LIKE...
MijickPopups Hero

  Popups Alerts Resizable Sheets Banners

SwiftUI Tooltip

This package provides you with an easy way to show tooltips over any SwiftUI view, since Apple does not provide ...

SimpleToast for SwiftUI

SimpleToast is a simple, lightweight, flexible and easy to use library to show toasts / popup notifications inside iOS or ...

SSToastMessage

Create Toast Views with Minimal Effort in SwiftUI Using SSToastMessage. SSToastMessage enables you to effortlessly add toast notifications, alerts, and ...

ToastUI

A simple way to show toast in SwiftUI   Getting Started • Documentation • Change Log