{"id":762,"url":"https://github.com/giginet/Crossroad","last_synced_at":"2025-08-06T12:31:53.142Z","repository":{"id":32488571,"uuid":"133929129","full_name":"giginet/Crossroad","owner":"giginet","description":":oncoming_bus: Route URL schemes easily","archived":false,"fork":false,"pushed_at":"2024-02-19T09:53:14.000Z","size":390,"stargazers_count":420,"open_issues_count":3,"forks_count":23,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-12-08T19:16:57.723Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/giginet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-05-18T08:54:11.000Z","updated_at":"2024-10-15T03:05:06.000Z","dependencies_parsed_at":"2024-06-19T05:17:30.360Z","dependency_job_id":"bb14ec09-1b64-45ba-b5b9-796c3bb54708","html_url":"https://github.com/giginet/Crossroad","commit_stats":{"total_commits":266,"total_committers":11,"mean_commits":"24.181818181818183","dds":"0.10902255639097747","last_synced_commit":"c10236689f467fe9a1f537e6c799d0be114c8b09"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/giginet%2FCrossroad","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/giginet%2FCrossroad/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/giginet%2FCrossroad/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/giginet%2FCrossroad/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/giginet","download_url":"https://codeload.github.com/giginet/Crossroad/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228898282,"owners_count":17988652,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-01-05T20:15:30.776Z","updated_at":"2024-12-09T13:30:34.058Z","avatar_url":"https://github.com/giginet.png","language":"Swift","readme":"[![Crossroad](Documentation/logo.png)](https://github.com/giginet/Crossroad)\n\n[![Build Status](https://img.shields.io/github/workflow/status/giginet/Crossroad/Crossroad?style=flat-square)](https://github.com/giginet/Crossroad/actions?query=workflow%3ACrossroad)\n[![Language](https://img.shields.io/static/v1.svg?label=language\u0026message=Swift%205.4\u0026color=FA7343\u0026logo=swift\u0026style=flat-square)](https://swift.org)\n[![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat-square)](https://swift.org/package-manager/) \n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat-square)](https://github.com/Carthage/Carthage) \n[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Crossroad.svg?style=flat-square)](http://cocoapods.org/pods/Crossroad)\n[![Platform](https://img.shields.io/static/v1.svg?label=platform\u0026message=iOS|macOS|tvOS\u0026color=grey\u0026logo=apple\u0026style=flat-square)](http://cocoapods.org/pods/Crossroad)\n[![License](https://img.shields.io/cocoapods/l/Crossroad.svg?style=flat-square)](https://github.com/giginet/Crossroad/blob/master/LICENSE)\n\nRoute URL schemes easily.\n\nCrossroad is an URL router focused on handling Custom URL Schemes or Universal Links.\nOf cource, you can also use for [Firebase Dynamic Link](https://firebase.google.com/docs/dynamic-links) or other similar services.\n\nUsing this, you can route multiple URL schemes and fetch arguments and parameters easily.\n\nThis library is developed in working time for Cookpad.\n\n## Basic Usage\n\nYou can use `DefaultRouter` to define route definitions.\n\nImagine to implement Pokédex on iOS. You can access somewhere via URL scheme.\n\n```swift\nimport Crossroad\n\nlet customURLScheme: LinkSource = .customURLScheme(\"pokedex\")\nlet universalLink: LinkSource = .universalLink(URL(string: \"https://my-awesome-pokedex.com\")!)\n\ndo {\n    let router = try DefaultRouter(accepting: [customURLScheme, universalLink]) { registry in\n        registry.route(\"/pokemons/:pokedexID\") { context in \n            let pokedexID: Int = try context.argument(named: \"pokedexID\") // Parse 'pokedexID' from URL\n            if !Pokedex.isExist(pokedexID) { // Find the Pokémon by ID\n                throw PokedexError.pokemonIsNotExist(pokedexID) // If Pokémon is not found. Try next route definition.\n            }\n            presentPokedexDetailViewController(of: pokedexID)\n        }\n        registry.route(\"/pokemons\") { context in \n            let type: Type? = context.queryParameters.type // If URL contains \u0026type=fire, you can get Fire type.\n            presentPokedexListViewController(for: type)\n        }\n\n        // ...\n    }\n} catch {\n    // If route definitions have some problems, routers fail initialization and raise reasons.\n    fatalError(error.localizedDescription)\n}\n\n// Pikachu(No. 25) is exist! so you can open Pikachu's page.\nlet canRespond25 = router.responds(to: URL(string: \"pokedex://pokemons/25\")!) // true\n// No. 9999 is missing. so you can't open this page.\nlet canRespond9999 = router.responds(to: URL(string: \"pokedex://pokemons/9999\")!) // false\n// You can also open the pages via universal links.\nlet canRespondUniversalLink = router.responds(to: URL(string: \"https://my-awesome-pokedex.com/pokemons/25\")!) // true\n\n// Open Pikachu page\nrouter.openIfPossible(URL(string: \"pokedex://pokemons/25\")!)\n// Open list of fire Pokémons page\nrouter.openIfPossible(URL(string: \"pokedex://pokemons?type=fire\")!)\n```\n\n### Using AppDelegate\n\nIn common use case, you should call `router.openIfPossible` on `UIApplicationDelegate` method.\n\n```swift\nfunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -\u003e Bool {\n    if router.responds(to: url, options: options) {\n        return router.openIfPossible(url, options: options)\n    }\n    return false\n}\n```\n\n### Using SceneDelegate\n\nOr, if you are using `SceneDelegate` with a modern app:\n\n```swift\nfunc scene(_ scene: UIScene, openURLContexts URLContexts: Set\u003cUIOpenURLContext\u003e) {\n    guard let context = URLContexts.first else {\n        return\n    }\n    router.openIfPossible(context.url, options: context.options)\n}\n```\n\n### Using NSApplicationDelegate (for macOS)\n\nIf you develop macOS applications:\n\n```swift\nclass AppDelegate: NSObject, NSApplicationDelegate {\n    func applicationDidFinishLaunching(_ aNotification: Notification) {\n        let appleEventManager = NSAppleEventManager.shared()\n        appleEventManager.setEventHandler(self,\n                                          andSelector: #selector(handleURLEvent(event:replyEvent:)),\n                                          forEventClass: AEEventClass(kInternetEventClass),\n                                          andEventID: AEEventID(kAEGetURL))\n    }\n\n    @objc func handleURLEvent(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) {\n        guard let urlString = event?.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue else { return }\n        guard let url = URL(string: urlString) else { return }\n        router.openIfPossible(context.url, options: [:])\n    }\n}\n```\n\n## Argument and Parameter\n\n### Argument\n\n`:` prefixed components on passed URL pattern mean **argument**.\n\nFor example, if passed URL matches `pokedex://search/:keyword`, you can get `keyword` from `Context`.\n\n```swift\n// actual URL: pokedex://search/Pikachu\nlet keyword: String = try context.arguments(named: \"keyword\") // Pikachu\n```\n\n### QueryParameter\n\nAnd more, you can get query parameters if exist.\n\n```swift\n// actual URL: pokedex://search/Pikachu?generation=1\nlet generation: Int? = context.queryParameters[\"generation\"] // 1\n// or you can also get value using DynamicMemberLookup\nlet generation: Int? = context.queryParameters.generation // 1\n```\n\nYou can cast arguments/query parameters as any type. Crossroad attempt to cast each String values to the type.\n\n```swift\n// expected pattern: pokedex://search/:pokedexID\n// actual URL: pokedex://search/25\nlet pokedexID: Int = try context.arguments(named: \"keyword\") // 25\n```\n\nCurrently supported types are `Int`, `Int64`, `Float`, `Double`, `Bool`, `String` and `URL`.\n\n### Enum arguments\n\nYou can use enums as arguments by conforming to `Parsable`.\n\n```swift\nenum Type: String, Parsable {\n    case normal\n    case fire\n    case water\n    case grass\n    // ....\n}\n\n// matches: pokedex://pokemons?type=fire\nlet type: Type? = context.queryParameters.type // .fire\n```\n\n### Comma-separated list\n\nYou can treat comma-separated query strings as `Array` or `Set`.\n\n```swift\n// matches: pokedex://pokemons?types=water,grass\nlet types: [Type]? = context.queryParameters.types // [.water, .grass]\n```\n\n### Custom argument\n\nYou can also define own arguments by implementing `Parsable`.\nThis is an example to parse custom struct.\n\n```swift\nstruct User {\n    let name: String\n}\nextension User: Parsable {\n    init?(from string: String) {\n        self.name = string\n    }\n}\n```\n\n## Multiple link sources support\n\nYou can define complex routing definitions like following:\n\n```swift\nlet customURLScheme: LinkSource = .customURLScheme(\"pokedex\")\nlet pokedexWeb: LinkSource = .universalLink(URL(string: \"https://my-awesome-pokedex.com\")!)\nlet anotherWeb: LinkSource = .universalLink(URL(string: \"https://kanto.my-awesome-pokedex.com\")!)\n\nlet router = try DefaultRouter(accepting: [customURLScheme, pokedexWeb, anotherWeb]) { registry in\n    // Pokémon detail pages can be opened from all sources.\n    registry.route(\"/pokemons/:pokedexID\") { context in \n        let pokedexID: Int = try context.argument(named: \"pokedexID\") // Parse 'pokedexID' from URL\n        if !Pokedex.isExist(pokedexID) { // Find the Pokémon by ID\n            throw PokedexError.pokemonIsNotExist(pokedexID)\n        }\n        presentPokedexDetailViewController(of: pokedexID)\n    }\n\n    // Move related pages can be opened only from Custom URL Schemes\n    registry.group(accepting: [customURLScheme]) { group in\n        group.route(\"/moves/:move_name\") { context in \n            let moveName: String = try context.argument(named: \"move_name\")\n            presentMoveViewController(for: moveName)\n        }\n        group.route(\"/pokemons/:pokedexID/move\") { context in \n            let pokedexID: Int = try context.argument(named: \"pokedexID\")\n            presentPokemonMoveViewController(for: pokedexID)\n        }\n    }\n\n    // You can pass acceptPolicy for a specific page.\n    registry.route(\"/regions\", accepting: .only(for: pokedexWeb)) { context in \n        presentRegionListViewController()\n    }\n}\n```\n\nThis router can treat three link sources.\n\n## Custom Router\n\nYou can add any payload to `Router`.\n\n```swift\nstruct UserInfo {\n    let userID: Int64\n}\nlet router = try Router\u003cUserInfo\u003e(accepting: customURLScheme) { registry in\n    registry.route(\"pokedex://pokemons\") { context in \n        let userInfo: UserInfo = context.userInfo\n        let userID = userInfo.userID\n    }\n    // ...\n])\nlet userInfo = UserInfo(userID: User.current.id)\nrouter.openIfPossible(url, userInfo: userInfo)\n```\n\n## Parse URL patterns\n\nIf you maintain a complex application and you want to use independent URL pattern parsers without Router.\nYou can use `ContextParser`.\n\n```swift\nlet parser = ContextParser()\nlet context = parser.parse(URL(string: \"pokedex:/pokemons/25\")!, \n                           with: \"pokedex://pokemons/:id\")\n```\n\n## Installation\n\n### Swift Package Manager\n\n- File \u003e Swift Packages \u003e Add Package Dependency\n- Add https://github.com/giginet/Crossroad.git\n    Select \"Up to Next Major\" with \"4.0.0\"\n\n### CocoaPods\n\n```ruby\nuse_frameworks!\n\npod 'Crossroad'\n```\n\n### Carthage\n\n```\ngithub \"giginet/Crossroad\"\n```\n\n## Demo\n\n1. Open `Demo/Demo.xcodeproj` on Xcode.\n2. Build `Demo` schema.\n\n## Supported version\n\nLatest version of Crossroad requires Swift 5.2 or above.\n\nUse 1.x instead on Swift 4.1 or lower.\n\n|Crossroad Version|Swift Version|Xcode Version|\n|-----------------|-------------|-------------|\n|4.x              |5.4          |Xcode 13.0   |\n|3.x              |5.0          |Xcode 10.3   |\n|2.x              |5.0          |Xcode 10.2   |\n|1.x              |4.0 ~ 4.2    |~ Xcode 10.1 |\n\n## License\n\nCrossroad is released under the MIT License.\n\nHeader logo is released under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. Original design by [@Arslanshn](https://github.com/Arslanshn).\n","funding_links":[],"categories":["App Routing","Libs","Swift","App Routing [🔝](#readme)"],"sub_categories":["App Routing","Getting Started"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgiginet%2FCrossroad","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgiginet%2FCrossroad","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgiginet%2FCrossroad/lists"}