Contents

giginet/crossroad

Route URL schemes easily.

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

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

  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 |

License

Crossroad is released under the MIT License.

Header logo is released under the CC BY 4.0 license. Original design by @Arslanshn.

Package Metadata

Repository: giginet/crossroad

Default branch: master

README: README.md