{"id":13465343,"url":"https://github.com/nikans/MonarchRouter","last_synced_at":"2025-03-25T16:31:37.572Z","repository":{"id":56625842,"uuid":"157981731","full_name":"nikans/MonarchRouter","owner":"nikans","description":"Declarative URL- and state-based router written in Swift.","archived":false,"fork":false,"pushed_at":"2021-03-22T16:57:00.000Z","size":1420,"stargazers_count":42,"open_issues_count":0,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-09-21T16:02:13.110Z","etag":null,"topics":["coordinator","hacktoberfest","ios","ios-route","ios-routing","monarch-router","navigation","redux","reswift","router","swift"],"latest_commit_sha":null,"homepage":"https://nikans.com","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/nikans.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}},"created_at":"2018-11-17T12:19:41.000Z","updated_at":"2024-09-12T17:39:49.000Z","dependencies_parsed_at":"2022-08-15T22:10:38.821Z","dependency_job_id":null,"html_url":"https://github.com/nikans/MonarchRouter","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikans%2FMonarchRouter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikans%2FMonarchRouter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikans%2FMonarchRouter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikans%2FMonarchRouter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nikans","download_url":"https://codeload.github.com/nikans/MonarchRouter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222088539,"owners_count":16928976,"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":["coordinator","hacktoberfest","ios","ios-route","ios-routing","monarch-router","navigation","redux","reswift","router","swift"],"created_at":"2024-07-31T15:00:27.892Z","updated_at":"2024-10-29T17:30:45.425Z","avatar_url":"https://github.com/nikans.png","language":"Swift","funding_links":[],"categories":["Libs","App Routing [🔝](#readme)","Architecture and State"],"sub_categories":["App Routing"],"readme":"# MonarchRouter\n\n[![Version](https://img.shields.io/cocoapods/v/MonarchRouter.svg?style=flat)](https://cocoapods.org/pods/MonarchRouter)\n[![License](https://img.shields.io/cocoapods/l/MonarchRouter.svg?style=flat)](https://cocoapods.org/pods/MonarchRouter)\n[![Platform](https://img.shields.io/cocoapods/p/MonarchRouter.svg?style=flat)](https://cocoapods.org/pods/MonarchRouter)\n\n![Monarch Router](https://github.com/nikans/MonarchRouter/blob/master/Media/logo@2x.png)\n\n\n*Powerful functional state-based router written in Swift.* \n\nMonarch Router is a declarative routing handler that is capable of managing complex View Controllers hierarchy transitions automatically, decoupling View Controllers from each other via Coordinator and Presenters. It fits right in with Redux style state flow and reactive frameworks.\n\nThe Coordinator is constructed by declaring a route hierarchy mapped with a URL structure. Presenters abstract UI creation and modification. Common URL-handling conventions are used for routing. It's designed for you to feel at home if you ever developed a server-side app routing.\n\nMonarch Router is distributed via [SPM](#swift-package-manager) and [Cocoapods](#cocoapods).\n\n\u003e Monarch butterflies weight less than 1 gram but cover thousands of miles during their migration. It's considered an iconic pollinator and one of the most beautiful butterfly species.\n\n\n## Features\n\n- [x] Navigating complex View Controllers hierarchy automatically — frome anywhere to anywhere in your app.\n- [x] Parsing and passing route parameters to endpoint View Controllers, following URL conventions.\n- [x] Deeplinking to handle Push Notifications, Shortcuts and Universal Links.\n- [x] Navigating forks (tabbar-like presenters).\n- [x] Navigating stacks (i.e. navigation controller).\n- [x] Opening and dismissing modals, with their own hierarchy.\n- [x] Switching top-level app sections via changing the window's rootViewController.\n- [x] Scenes handling.\n- [ ] Handling navigation in universal apps. *(PRs welcome!)*\n- [ ] Properly abstracting Router layer to handle navigation in macOS apps.\n\n\n## Glossary\n\n- `Router`: your app's routing Coordinator (root `RoutingNode` with children); or more broadly speaking, this whole thing. \n- `RoutingNode`: a structure that collects functions together that are related to the same endpoint or intermidiate routing point with children. Each `RoutingNode` also requires a `Presenter`, to which any required changes are passed.\n- `RoutePresenter`: a structure used to create and configure a `Presentable` (i.e. `UIViewController`). There're several types of presenters: endpoint, stack (for navigation tree), fork (for tabs), switcher (for uncoupled apps sections).\n- `Presentable`: an actual object to be displayed (i.e. `UIViewController`).\n- Lazy Presenter: a lazy wrapper around a presenter creation function that wraps presenter scope, but the `Presentable` does not get created until invoked.\n\n- `RoutingRequest`: a URL or URL-like structure used to define the endpoint you want to navigate to.\n- `Route`: a structure that defines matching rules for a `RoutingRequest` to trigger routing to a certain `RoutingNode`.\n\n- `RouterStore`: holds the State for the router. Provides a method to dispatch a `RoutingRequest` and modify the State via a Reducer.\n- `RouterState`: holds the stack of active `RoutingNode`s.\n- `RouterReducer`: a function to calculate a new State. Implements navigation via `RoutingNode`'s callback. Unwinds unused `RoutingNode`s.\n\n\n## Basic flow\n\n1. `RouteRequest` is dispatched on a `RouterStore`. The request is a URL, or URL-like structure. \n2. The new State is calculated by a reducer, matching the request against a Coordinator hierarchy. Each Node in the hierarchy is associated with a `Route` (a matching rule) and a `Presenter` that abstracts the UI.\n3. Unused nodes and corresponding presentables are being unwound, presentables hierarchy reloaded based on caclulated State.\n\n\n## Example\n\nThe example project illustrates the basic usage of the router, as well as some not-trivial use cases, such as modals handling.\n\nIf you prefer using Cocoapods, rather than SPM, clone the repo, and run `pod install` from the Example directory first.\n\n**See [Example App](https://github.com/nikans/MonarchRouter/tree/master/Example).**\n\n\n## How to use\n\n### 0. Start with creating a `RouterStore`. \nPersist it in your App- or SceneDelegate.\n\n```swift\n// Initializing Router and setting root VC\nlet coordinator = appCoordinator()\nlet router = RouterStore(router: coordinator)\n\nself.appRouter = router\n```\n\n\n### 1. Define your App's `Routes`. \nRoutes are rules used to match against `RoutingRequest`s. \n\n```swift\n/// Your app custom Routes\nenum AppRoute: String, RouteType\n{\n    case login = \"login\"\n    case today = \"today\"\n    case story = \"today/story/:type/:id\"\n    case books = \"books\"\n    case book  = \"books/:id\"\n}\n```\n\nA route is consisted of `RouteComponent`s. These components are matched to the `RouteRequest`'s `PathComponent`s (see below).\n\nThere are several ways to define a `Route`:\n\n\n#### `String` conforms to `RouteType`.\n\n- Components are separated with `/`\n- Constant components are just strings (i.e. `login`)\n- Parameter components are prefixed with `:`\n- To match anything for a component use `:_`\n- To match everything to the end of the path use `...`\n\n\n#### Use built-in `RouteString` structure to create RegExp-validated routes.\n\n```swift\ntypealias ParameterValidation = (name: String, pattern: String)\ninit(_ predicate: String, parametersValidation: [ParameterValidation]? = nil)\n```\n\n- Use the stated above rules to set a predicate string.\n- Optionally add a `ParameterValidation` array, where `name` is a parameter name (without `:`) and `pattern` is a RegExp.\n\n\n#### Array of `RouteComponent`s conforms to `RouteType`.\n\nBuild your `Route` with `RouteComponent` enum:\n\n```swift\nenum RouteComponent \n{\n    /// Matches a constant component\n    case constant(String)\n    \n    /// Matches a parameter component\n    /// - parameter name: parameter name to match\n    /// - parameter isMatching: optional closure to match parameter value\n    case parameter(name: String, isMatching: ((_ value: Any) -\u003e Bool)? )\n    \n    /// Matches any path component for a route component\n    case anything\n    \n    /// Matches any path to the end\n    case everything\n}\n```\n\n\n### 1.1. Optionally define a set of `RoutingRequest`s. \n\n`RoutingRequest` is matched against `Route`s associated with a `RoutingNode`. \n\nTo make things easy, Monarch Router uses  `URL`s or valid URL-like `String`s to trigger routing. \n\nURL parts available:\n- path components (`books/:id`)\n- query items (`?name=eliah`)\n- fragment (`#documentation`)\n\n\nYou can dispatch `URL` or `String` directly. Alternatively you can create a custom enum:\n\n```swift\nenum AppRoutingRequest: RoutingRequestType\n{\n    case login\n    case today\n    case story(type: String, id: Int, title: String)\n    case books\n    case book(id: Int, title: String?)\n\n    var request: String {\n        switch self {\n        case .login:\n        return \"login\"\n\n        case .today:\n        return \"today\"\n\n        case .story(let type, let id, let title):\n        return \"today/story/\\(type)/\\(id)?title=\\(title)\"\n\n        case .books:\n        return \"books\"\n\n        case .book(let id, let title):\n        return \"books/\\(id)?title=\\(title ?? \"\")\"\n    }\n}\n```\n\nIf for your convenience you've decided to define a custom `RoutingRequestType` enum, you'll need a resolver function. Since here we're mapping our requests to a `String`, we'll use it's built-in resolver.\n\n```swift\nfunc resolve(for route: RouteType) -\u003e RoutingResolvedRequestType {\n    return request.resolve(for: route)\n}\n```\n\nMatched Presenters can be parametrized with resolved `RouteParameters` object (see below). \n\n\u003e Only path parameters are used for matching, though you can configure your presentable based on query parameters and fragment.\n\n\n### 1.2. Dispatch routing requests on the `RouterStore` object \n\n```swift\nrouter.dispatch(.login)\n```\n\nYou may want to hide your `RouterStore` implementation behind a specialized `ProvidesRouteDispatch` protocol, i.e:\n\n```swift\nprotocol ProvidesRouteDispatch: class\n{\n    /// Extension method to change the Route.\n    /// - parameter request: `AppRoutingRequest` to navigate to.\n    func dispatch(_ request: AppRoutingRequest)\n}\n\nextension RouterStore: ProvidesRouteDispatch { \n    func dispatch(_ request: AppRoutingRequest) {\n        dispatch(request.request)\n    }\n}\n```\n\nBut first we need to create a Coordinator.\n\n\n### 2. Create your app's Coordinator\n\nThe Coordinator is a hierarchial `RoutingNode` structure. \n\n```swift\n/// Creating the app's Coordinator hierarchy.\nfunc appCoordinator() -\u003e RoutingNodeType\n{    \n    return\n    \n    // Top level app sections' switcher\n    RoutingNode(sectionsSwitcherRoutePresenter()).switcher([\n\n        // Login \n        // (section 0)\n        RoutingNode(lazyPresenter(for: .login))\n            .endpoint(AppRoute.login),\n\n        // Tabbar \n        // (section 1)\n        RoutingNode(lazyTabBarRoutePresenter()).fork([\n\n                // Today nav stack\n                // (tab 0)\n                RoutingNode(lazyNavigationRoutePresenter()).stack([\n\n                    // Today\n                    RoutingNode(lazyPresenter(for: .today))\n                        .endpoint(AppRoute.today, modals: [\n\n                        // Story \n                        // (presented modally)\n                        RoutingNode(lazyPresenter(for: .story))\n                            .endpoint(AppRoute.story)\n                    ])\n                ]),\n\n                // Books nav stack\n                // (tab 1)\n                RoutingNode(lazyNavigationRoutePresenter()).stack([\n\n                    // Books\n                    // (master)\n                    RoutingNode(lazyPresenter(for: .books))\n                        .endpoint(AppRoute.books, children: [\n\n                        // Book\n                        // (detail)\n                        RoutingNode(lazyPresenter(for: .book))\n                            .endpoint(AppRoute.book)\n                        ])\n                ])\n            ])\n    ])\n}\n```\n\nDepending on it's `Presenter`, a `RoutingNode` can execute one of four types of behavior: \n- endpoint\n- stack (i.e. navigation tree)\n- fork (i.e. tabs)\n- switcher (decoupled app's sections)\n\nEach `RoutingNode` either matches a `RoutingRequest` against it's `Route` (i.e. `.endpoint(AppRoute.today)`) or against its childrens' routes (not-endpoint type nodes). The suitable sub-hierarchy is then selected, the `RouterState` is reduced to a new one. \n\nThe new nodes stack's `Presenter`s are then instantiating their `Presentable`s (i.e. `UIViewController`s) if necessary, and the app's navigation hierarchy is rebuilt automatically. \n\nUI magic is abstracted in the Presenters.\n\n\n### 3. Create `Presenter`s\n\n#### Presentable configuration\n\nThe main goal of presenters is to create a `Presentable` object. So, when you define a `Presenter` you have to pass a closure for the creation of a Presentable: `getPresentable: () -\u003e (UIViewController)`. \nCurrently, only `UIViewController` subtypes are supported.\n\nIf a `Presenter` was called with some `RouteParameter`s, an optional closure allowing for the `Presentable` configuration is called: `setParameters: ((_ parameters: RouteParameters, _ presentable: UIViewController) -\u003e ())`.\n\n\u003e *Note*: Conform your Presentable to `RouteParametrizedPresentable` to handle this automatically. \n\nAn optional closure `unwind: (_ presentable: UIViewController) -\u003e ()` is called when the node is no longer selected. You can set it if your Presentable requires any special behavior. \n\n**Important**: Every `Presenter` can be instantiated directly or lazily. It's advised to use lazy initialization in your Coordinator hierarchy, otherwise all the presentables will be instantiated on the app launch.\n\n\n#### Built-in Presenters\n\n##### `RoutePresenter` \nis used for endpoint presentation.\n\nThe endpoint Presenter is able to present and dismiss modals with the hierarchy of their own. The corresponding closures are called: \n\n```swift\n/// Callback executed when a modal view is requested to be presented.\npresentModal: (_ modal: UIViewController, _ over: UIViewController) -\u003e ()\n\n/// Callback executed when a presenter is required to close its modal.\ndismissModal: ((_ modal: UIViewController)-\u003e())\n```\nModal presentation works out of the box, so you may want to use those for the special behavior only.\n\n##### `RoutePresenterFork` \nis used for tabbar-style presentation. \n\nSpecial closures are used to configurate a Presentable (i.e. `UITabBarController`) \n```swift\n/// Sets the options for Router to choose from\nsetOptions: (_ options: [UIViewController], _ container: UIViewController) -\u003e ()\n\n/// Sets the specified option as currently selected.\nsetOptionSelected: (_ option: UIViewController, _ container: UIViewController) -\u003e ()\n```\n\u003e Use `.junctionsOnly` dispatch option when switching to a tab by it's root Route, when the tab already contains presented stack.\n\n##### `RoutePresenterStack`\nis used to organize other Presenters in a navigation stack (i.e. `UINavigationController`).\n\n```swift\n/// Sets the navigation stack\nsetStack: (_ stack: [UIViewController], _ container: UIViewController) -\u003e ()\n\n/// Presets root Presentable when the stack's own Presentable is requested\nprepareRootPresentable: (_ rootPresentable: UIViewController, _ container: UIViewController) -\u003e ()\n```\n\n##### `RoutePresenterSwitcher`\nis used to switch between decoupled app sections (i.e. login sequence, main sequence...)\n\n```swift\n/// Sets the specified option as currently selected.\nsetOptionSelected: (_ option: UIViewController) -\u003e ()\n```\n\nThis Presenter may probably don't have a Presentable.\n\n\n#### Example Presenters\n\nThe Example app contains several useful Presenters, not made part of the library, i.e: \n\n- `UITabBarController` presenter built on `RoutePresenterFork` with a delegate to dispatch routing request on tap.\n- `UINavigationController` presenter built on `RoutePresenterStack` with relevant pop/push/etc behavior.\n- Sections switch presenter built on `RoutePresenterSwitcher`, with ability to set `window`'s `rootViewController`.\n\n\n## Principle concepts\n\n### UI is a representation of State\n\nAs the State changes over time, so will the UI projection of that State.\nGiven any State value the UI must be predictable and repeatable.\n\n### Device dependent state should be separate from the router State.\n\nDisplaying the same State on a phone and tablet for example, can result in different UIs. The device dependent state should remain on that device. An OS X and iOS app can use the same State and logic classes and interchange Routers for representing the State.\n\n*Not fully implemented yet. PRs welcome!*\n\n### UI can generate actions to update the nodes stack in the State\n\nThe user tapping a back button is easy to capture and generate an action that updates the State, which causes the UI change. But a user 'swiping' back a view is harder to capture. It should instead generate an action on completion to update the State. Then, if the current UI already matches the new State no UI changes are necessary.\n\n\n## Installation\n\n### Swift Package Manager\n\nUsing Xcode UI: go to your Project Settings -\u003e Swift Packages and add `git@github.com:nikans/MonarchRouter.git` there.\n\nTo integrate using Apple's Swift package manager, without Xcode integration, add the following as a dependency to your Package.swift:\n\n```swift\n.package(url: \"git@github.com:nikans/MonarchRouter.git\", .upToNextMajor(from: \"1.1.2\"))\n```\n\n### CocoaPods\n\nTo install it, simply add the following line to your Podfile:\n\n```ruby\npod 'MonarchRouter', '~\u003e 1.1.2'\n```\n\nYou may find the last release version [here](https://github.com/nikans/MonarchRouter/releases).\n\n\n## Requirements\n\nCurrently only iOS/iPhone 8.0+ is properly supported, but theoretically it's easyly extended to support Universal apps. MacOS support requires a new layer of abstraction with generics and stuff, and I think that it's clearer to use as it is for now. *But you are very welcome to contribute!*\n\n- [x] iOS/iPhone\n- [ ] iOS/Universal\n- [ ] macOS\n\n\n## Author\n\nEliah Snakin: eliah@nikans.com\n\nMonarch Router emerged from crysalis of [Featherweight Router](https://github.com/FeatherweightLabs/FeatherweightRouter).\n\n\n## License\n\nMonarchRouter is available under the MIT license. See the LICENSE file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnikans%2FMonarchRouter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnikans%2FMonarchRouter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnikans%2FMonarchRouter/lists"}