{"id":17997129,"url":"https://github.com/rockbruno/routerservice","last_synced_at":"2025-04-06T21:14:57.249Z","repository":{"id":43678435,"uuid":"236832815","full_name":"rockbruno/RouterService","owner":"rockbruno","description":"💉Type-safe Navigation/Dependency Injection Framework for Swift","archived":false,"fork":false,"pushed_at":"2021-11-24T19:57:37.000Z","size":329,"stargazers_count":339,"open_issues_count":3,"forks_count":26,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-03-30T19:08:28.630Z","etag":null,"topics":["architecture","hacktoberfest","ios","modular","navigation","swift","xcode"],"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/rockbruno.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":"2020-01-28T20:20:34.000Z","updated_at":"2025-02-18T16:20:25.000Z","dependencies_parsed_at":"2022-09-05T03:40:43.226Z","dependency_job_id":null,"html_url":"https://github.com/rockbruno/RouterService","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockbruno%2FRouterService","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockbruno%2FRouterService/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockbruno%2FRouterService/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockbruno%2FRouterService/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rockbruno","download_url":"https://codeload.github.com/rockbruno/RouterService/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247550690,"owners_count":20956987,"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":["architecture","hacktoberfest","ios","modular","navigation","swift","xcode"],"created_at":"2024-10-29T21:17:03.228Z","updated_at":"2025-04-06T21:14:57.232Z","avatar_url":"https://github.com/rockbruno.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RouterService\n\n```swift\nstruct SwiftRocksFeature: Feature {\n\n    @Dependency var client: HTTPClientProtocol\n    @Dependency var persistence: PersistenceProtocol\n    @Dependency var routerService: RouterServiceProtocol\n\n    func build(fromRoute route: Route?) -\u003e UIViewController {\n        return SwiftRocksViewController(\n            client: client,\n            persistence: persistence,\n            routerService: routerService,\n        )\n    }\n}\n```\n\nRouterService is a type-safe navigation/dependency injection framework focused on making modular Swift apps have **very fast build times**. \u003ca href=\"https://speakerdeck.com/amiekweon/the-evolution-of-routing-at-airbnb\"\u003eBased on the system used at AirBnB presented at BA:Swiftable 2019.\u003c/a\u003e\n\nRouterService is meant to be used as a dependency injector for \u003ca href=\"https://swiftrocks.com/reducing-ios-build-times-by-using-interface-targets\"\u003emodular apps where each targets contains an additional \"interface\" module.\u003c/a\u003e The linked article contains more info about that, but as a summary, this parts from the principle that a feature module should never directly depend on concrete modules. Instead, for build performance reasons, a feature only has access to another feature's **interface**, which contains protocols and other things that are unlikely to change. To link everything together, RouterService takes care of injecting the necessary concrete dependencies whenever one of these protocols is referenced.\n\nThe final result is:\n - An app with a horizontal dependency graph (very fast build times!)\n - Dynamic navigation (any screen can be pushed from anywhere!)\n\nFor more information on this architecture, \u003ca href=\"https://swiftrocks.com/reducing-ios-build-times-by-using-interface-targets\"\u003echeck the related SwiftRocks article.\u003c/a\u003e\n\n## How RouterService Works\n\n*(For a complete example, check this repo's example app.)*\n\nRouterService works through the concept of `Features` -- which are `structs` that can create ViewControllers after being given access to whatever dependencies it needs to do that. Here's an example of how we can create an \"user profile feature\" using this format.\n\nThis feature requires access to a HTTP client, so we'll first define that. Since a modular app with interface targets should separate the protocol from the implementation, our first module will be a `HTTPClientInterface` that exposes the client protocol:\n\n```swift\n// Module 1: HTTPClientInterface\n\npublic protocol HTTPClientProtocol: AnyObject { /* Client stuff */ }\n```\n\nFrom the interface, let's now create a **concrete** `HTTPClient` module that implements it:\n\n```swift\n// Module 2: HTTPClient\n\nimport HTTPClientInterface\n\nprivate class HTTPClient: HTTPClientProtocol { /* Implementation of the client stuff */ }\n```\n\nWe're now ready to define our Profile RouterService feature. At a new `Profile` module, we can create a `Feature` struct that has the client's protocol as a dependency.\nTo have access to the protocol, the `Profile` module will import the dependency's interface.\n\n```swift\n// Module 3: Profile\n\nimport HTTPClientInterface\nimport RouterServiceInterface\n\nstruct ProfileFeature: Feature {\n\n    @Dependency var client: HTTPClientProtocol\n\n    func build(fromRoute route: Route?) -\u003e UIViewController {\n        return ProfileViewController(client: client)\n    }\n}\n```\n\nBecause the `Profile` feature doesn't import the concrete `HTTPClient` module, changes made to them **will not recompile** the `Profile` module. Instead, RouterService will inject the concrete objects in runtime. If you multiply this by hundreds of protocols and dozens of features, you will get a massive build time improvement in your app!\n\nIn this case, the `Profile` feature will only be recompiled by external changes if the interface protocols themselves are changed -- which should be considerably rarer than changes to their concrete counterparts.\n\nLet's now see how we can tell RouterService to push `ProfileFeature`'s ViewController.\n\n## Routes\n\nInstead of pushing features by directly creating instances of their ViewControllers, in RouterService, the navigation is done completely through `Routes`. By themselves, `Routes` are just `Codable` structs that can hold contextual information about an action (like the previous screen that triggered this route, for analytics purposes). However, the magic comes from how they are used: `Routes` are paired with `RouteHandlers`: classes that define a list of supported `Routes` and the `Features` that should be pushed when they are executed. \n\nFor example, to expose the `ProfileFeature` shown above to the rest of the app, the hypothetical `Profile` target should first expose a route in a separate `ProfileInterface` target:\n\n```swift\n// Module 4: ProfileInterface\n\nimport RouterServiceInterface\n\nstruct ProfileRoute: Route {\n    static let identifier: String = \"profile_mainroute\"\n    let someAnalyticsContext: String\n}\n```\n\nNow, at the concrete `Profile` target, we can define a `ProfileRouteHandler` that connects it to the `ProfileFeature`.\n\n```swift\nimport ProfileInterface\nimport RouterServiceInterface\n\npublic final class ProfileRouteHandler: RouteHandler {\n    public var routes: [Route.Type] {\n        return [ProfileRoute.self]\n    }\n\n    public func destination(\n        forRoute route: Route,\n        fromViewController viewController: UIViewController\n    ) -\u003e Feature.Type {\n        guard route is ProfileRoute else {\n            preconditionFailure() // unexpected route sent to this handler\n        }\n        return ProfileFeature.self\n    }\n}\n```\n\n`RouteHandlers` are designed to handle multiple `Routes`. If a specific feature target contains multiple ViewControllers, you can have a single `RouteHandler` in that target that handles all of the possibles `Routes`.\n\nTo push a new `Feature`, all a `Feature` has to do is import the module that contains the desired `Route` and call the `RouterServiceProtocol` `navigate(_:)` method. `RouterServiceProtocol`, the interface protocol of the RouterService framework, can be added as a dependency of features for this purpose.\n\nAssuming we also created some hypothetical `LoginFeature` alongside our `ProfileFeature`, here's how we could push `LoginFeature`'s ViewController from the `ProfileFeature`:\n\n```swift\nimport LoginInterface\nimport RouterServiceInterface\nimport HTTPClientInterface\nimport UIKit\n\nfinal class ProfileViewController: UIViewController {\n    let client: HTTPClientProtocol\n    let routerService: RouterServiceProtocol\n\n    init(client: HTTPClientProtocol, routerService: RouterServiceProtocol) {\n        self.client = client\n        self.routerService = routerService\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    func goToLogin() {\n        let loginRoute = SomeLoginRouteFromTheLoginFeatureInterface()\n        routerService.navigate(\n            toRoute: loginRoute,\n            fromView: self,\n            presentationStyle: Push(),\n            animated: true\n        )\n    }\n}\n```\n\n## Wrapping everything up\n\nIf all features are isolated, how do you start the app?\n\nWhile the features are isolated from other concrete targets, you should have a \"main\" target that imports everything and everyone (for example, your AppDelegate). This should be the only target capable of importing concrete targets. \n\nFrom there, you can create a concrete instance of `RouterService`, register everyone's `RouteHandlers` and dependencies and start the navigation process loop by calling `RouterService's`: `navigationController(_:)` method (if you need a navigation), or by manually calling a `Feature's` `build(_:)` method.\n\n```swift\nimport HTTPClient\nimport Profile\nimport Login\n\nclass AppDelegate {\n\n   let routerService = RouterService()\n\n   func didFinishLaunchingWith(...) {\n\n       // Register Dependencies\n\n       routerService.register(dependencyFactory: { \n           return HTTPClient() \n       }, forType: HTTPClientProtocol.self)\n\n       // Register RouteHandlers\n\n       routerService.register(routeHandler: ProfileRouteHandler())\n       routerService.register(routeHandler: LoginRouteHandler())\n\n       // Setup Window\n\n       let window = UIWindow()\n       window.makeKeyAndVisible()\n\n       // Start RouterService\n\n       window.rootViewController = routerService.navigationController(\n        withInitialFeature: ProfileFeature.self\n       )\n\n       return true\n   }\n}\n```\n\nFor more information and examples, check the example app provided inside this repo. It contains an app with two features targets and a fake dependency target.\n\n## Memory Management of Dependencies\n\nDependencies are registered through closures (called \"dependency factories\") to allow RouterService to generate their instances on demand and deallocate them when no feature that needs them is active. This is done by having an internal store that holds the values weakly. The closures themselves are held in memory throughout the entire lifecycle of the app, but should be less impactful than holding the instances themselves.\n\n## Providing fallbacks for Features\n\nIf you need to control the availability of your feature, either because of a feature flag or because it has a minimum iOS version requirement, it's possible to handle it through a `Feature's` `isEnabled()` method.\nThis method provides information to `RouterService` about the availability of your feature. We really recommend you to have your toggle controls (Feature Flag Provider, Remote Config Provider, User Defaults, etc) as a `@Dependency` of your feature so you can easily use them to implement it and properly unit test it later.\nIf a `Feature` can be disabled, you need to provide a fallback by implementing the `fallback(_:)` method to allow RouterService to receive and present a valid context. For example:\n```swift\npublic struct FooFeature: Feature {\n\n    @Dependency var httpClient: HTTPClientProtocol\n    @Dependency var routerService: RouterServiceProtocol\n    @Dependency var featureFlag: FeatureFlagProtocol\n\n    public init() {}\n    \n    public func isEnabled() -\u003e Bool {\n        return featureFlag.isEnabled()\n    }\n    \n    public func fallback(forRoute route: Route?) -\u003e Feature.Type? {\n        return MyFallbackFeature.self\n    }\n\n    public func build(fromRoute route: Route?) -\u003e UIViewController {\n        return MainViewController(\n            httpClient: httpClient,\n            routerService: routerService\n        )\n    }\n}\n```\n\nIf a disabled feature attempts to be presented without a fallback, your app will crash. By default, all features are enabled and have no fallbacks.\n\n## AnyRoute\n\nAll `Routes` are Codable, but what if more than one route can be returned by the backend?\n\nFor this purpose, RouterServiceInterface provides a type-erased `AnyRoute` that can decode any registered `Route` from a specific string format. This allows you to have your backend dictate how navigation should be handled inside the app. Cool, right?\n\nTo use it, add `AnyRoute` (which is `Decodable`) to your backend's response model:\n\n```swift\nstruct ProfileResponse: Decodable {\n    let title: String\n    let anyRoute: AnyRoute\n}\n```\n\nBefore decoding `ProfileResponse`, inject your RouterService instance in the `JSONDecoder`: (necessary to determine which Route should be decoded)\n\n```swift\nlet decoder = JSONDecoder()\n\nrouterService.injectContext(toDecoder: decoder)\n\ndecoder.decode(ProfileResponse.self, from: data)\n```\n\nYou can now decode `ProfileResponse`. If the injected RouterService contains the Route returned by the backend, `AnyRoute` will successfully decode to it.\n\n```swift\nlet response = decoder.decode(ProfileResponse.self, from: data)\nprint(response.anyRoute.route) // Route\n```\n\nThe string format expected by the framework is a string in the `route_identifier|parameters_json_string` format. For example, to decode the `ProfileRoute` shown in the beginning of this README, `ProfileResponse` should look like this:\n\n```json\n{\n    \"title\": \"Profile Screen\",\n    \"anyRoute\": \"profile_mainroute|{\\\"analyticsContext\\\": \\\"Home\\\"}\"\n}\n```\n\n## Installation\n\nWhen installing RouterService, the interface module `RouterServiceInterface` will also be installed.\n\n### Swift Package Manager\n\n```swift\n.package(url: \"https://github.com/rockbruno/RouterService\", .upToNextMinor(from: \"1.1.0\"))\n```\n\n### CocoaPods\n\n```ruby\npod 'RouterService'\n```\n\n## Example Project\n\nThe [ExampleProject](ExampleProject) uses [XcodeGen](https://github.com/yonaskolb/XcodeGen) to generate its Xcode project. \n\nYou can either install the XcodeGen binary with Homebrew, or leverage the Swift Package Manager, which will clone, build and then run XcodeGen. The individual steps to generate the `RouterServiceExampleApp.xcodeproj` are outlined below.\n\n\u003cdetails open\u003e\n\u003csummary\u003eHomebrew\u003c/summary\u003e\n\n```bash\n# 1. Install XcodeGen using Homebrew\nbrew install xcodegen\n\n# 2. Run xcodegen from within the ExampleProject directory\ncd ExampleProject \u0026\u0026 xcodegen generate\n```\n\n\u003c/details\u003e\n\n\u003cdetails open\u003e\n\u003csummary\u003eSwift Package Manager\u003c/summary\u003e\n\n```bash\n# 1. Run xcodegen using SPM from within the ExampleProject directory\ncd ExampleProject \u0026\u0026 swift run xcodegen\n```\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frockbruno%2Frouterservice","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frockbruno%2Frouterservice","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frockbruno%2Frouterservice/lists"}