{"id":20336628,"url":"https://github.com/markbattistella/routingmanager","last_synced_at":"2025-04-11T22:41:05.335Z","repository":{"id":253068694,"uuid":"799087156","full_name":"markbattistella/RoutingManager","owner":"markbattistella","description":"A Swift package designed to simplify and enhance navigation in SwiftUI applications. It supports stateful navigation with persistent storage, allowing developers to manage complex navigation flows with ease.","archived":false,"fork":false,"pushed_at":"2025-02-06T05:11:19.000Z","size":103,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-20T12:16:40.622Z","etag":null,"topics":["hacktoberfest","ios","ipados","macos","navigation","navigationstack","observable","route","routes","routing","swift","swiftui","visionos","watchos"],"latest_commit_sha":null,"homepage":"https://swiftpackageindex.com/markbattistella/RoutingManager/documentation/","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/markbattistella.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},"funding":{"custom":["https://www.paypal.me/markbattistella/5AUD","https://www.paypal.me/markbattistella/10AUD","https://www.paypal.me/markbattistella/20AUD"]}},"created_at":"2024-05-11T06:31:54.000Z","updated_at":"2025-02-08T04:26:30.000Z","dependencies_parsed_at":"2024-08-14T09:15:50.053Z","dependency_job_id":null,"html_url":"https://github.com/markbattistella/RoutingManager","commit_stats":null,"previous_names":["markbattistella/routingmanager"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbattistella%2FRoutingManager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbattistella%2FRoutingManager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbattistella%2FRoutingManager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbattistella%2FRoutingManager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markbattistella","download_url":"https://codeload.github.com/markbattistella/RoutingManager/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248493022,"owners_count":21113159,"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":["hacktoberfest","ios","ipados","macos","navigation","navigationstack","observable","route","routes","routing","swift","swiftui","visionos","watchos"],"created_at":"2024-11-14T21:06:12.159Z","updated_at":"2025-04-11T22:41:05.317Z","avatar_url":"https://github.com/markbattistella.png","language":"Swift","funding_links":["https://www.paypal.me/markbattistella/5AUD","https://www.paypal.me/markbattistella/10AUD","https://www.paypal.me/markbattistella/20AUD"],"categories":[],"sub_categories":[],"readme":"\u003c!-- markdownlint-disable MD033 MD041 --\u003e\n\u003cdiv align=\"center\"\u003e\n\n# RoutingManager\n\n[![Swift Version][Shield1]](https://swiftpackageindex.com/markbattistella/RoutingManager)\n\n[![OS Platforms][Shield2]](https://swiftpackageindex.com/markbattistella/RoutingManager)\n\n[![Licence][Shield3]](https://github.com/markbattistella/RoutingManager/blob/main/LICENSE)\n\n\u003c/div\u003e\n\n`RoutingManager` is a Swift package designed to simplify and enhance navigation in SwiftUI applications. It provides a structured approach to managing navigation stacks, with support for state persistence and multiple storage options. It also includes robust error handling and flexible environment injection.\n\n## Features\n\n- **Stateful Navigation:** Manage navigation stacks with push, pop, reset, and replace operations.\n- **Persistent Storage:** Save and restore navigation state using in-memory, JSON-based file storage, or a custom storage implementation.\n- **Custom Storage Support:** Implement your own storage solutions by conforming to `FileStorageRepresentable`.\n- **Error Handling:** Handle navigation failures using the `NavigationError` enum and `navigationError` handler.\n- **Environment Injection:** Pass dependencies directly into views through `NavigationWrapper`.\n\n## Installation\n\n### Swift Package Manager\n\nTo add `RoutingManager` to your project, use the Swift Package Manager.\n\n1. Open your project in Xcode.\n\n1. Go to `File \u003e Add Packages`.\n\n1. In the search bar, enter the URL of the `RoutingManager` repository:\n\n    ```url\n    https://github.com/markbattistella/RoutingManager\n    ```\n\n1. Click `Add Package`\n\n## Usage\n\n### Getting Started\n\nTo start using `RoutingManager`, import the package and create a `RoutingManager` instance in your SwiftUI view. Here's a basic setup:\n\n```swift\nimport SwiftUI\nimport RoutingManager\n\ntypealias Routes = RoutingManager\u003cStack, Route\u003e\n\n@main\nstruct RouteTestApp: App {\n  var body: some Scene {\n    WindowGroup {\n      NavigationWrapper(\n        storage: .inMemory,\n        identifier: \"default\",\n        for: Route.self\n      ) {\n        // Your first view\n      } environmentInjection: { route in\n        // Environment injection\n      }\n    }\n  }\n}\n\nenum Stack: String, NavigationStackRepresentable {\n  var id: String { rawValue }\n  case main\n}\n\nenum Route: String, NavigationRouteRepresentable {\n  var id: String { String(describing: self) }\n\n  case home\n  case products\n  case item(id: Int)\n\n  var body: some View {\n    switch self {\n      case .home:\n        HomeView()\n      case .products:\n        ProductsList()\n      case .item(let id):\n        ItemDetail(id: id)\n    }\n  }\n}\n```\n\n### Initialising `NavigationWrapper`\n\nThe `NavigationWrapper` is a key component of the `RoutingManager` package, providing a way to set up and manage your app's navigation flow in a declarative manner.\n\nWhen initialising `NavigationWrapper`, you configure how your navigation stack is managed and stored within the app.\n\n```swift\nNavigationWrapper(\n  storage: .memory,\n  stack: Stack.main,\n  for: Route.self\n) {\n  // The initial view for the navigation stack\n} environmentInjection: { route in\n  // Inject dependencies or modify the environment for the route's view\n  route.body\n}\n```\n\n#### Parameters\n\n1. `storage: NavigationStorageOption`\n\n   - This parameter defines how the navigation state is stored. The `NavigationWrapper` supports multiple storage options:\n     - `.memory`: Navigation state is stored in memory. It is not and cannot be persisted across app launches.\n     - `.json`: Navigation state is stored in a JSON file. It is persisted across app launches if the user chooses.\n     - `.custom(FileStorage\u003c[Stack: [Route]]\u003e)`: Navigation state is stored in a custom storage implementation. The user chooses how to handle saving, and loading across app launches.\n\n1. `stack: NavigationStackRepresentable`\n\n   - This is the stack you wish to update routes to. Specifying a stack allows you to initialise multiple `NavigationWrapper` across your app and have it affect specific stacks. For example, a `.main` stack for your navigation, and another one for `.sheet` or different tabs in a `TabView`.\n\n1. `for: R.Type`\n\n   - This specifies the type of routes your navigation stack will handle. Typically, this is the enum that conforms to `NavigationRouteRepresentable`, defining all possible routes in your app. By providing the route type, `NavigationWrapper` can manage the navigation between different views based on the routes you define.\n\n1. `content: () -\u003e View`\n\n   - The trailing closure is where you provide the initial view or entry point for your navigation stack. This is the view that users will see when they first launch the app or when the navigation stack is reset. You can structure your navigation flow starting from this view, utilising the routes you've defined.\n\n1. `environmentInjection: (R) -\u003e View`\n\n   - This closure takes a `Route` instance as its parameter, which represents the current route being handled.\n   - Inside this closure, you can modify the environment or inject dependencies that are required by the view associated with the route.\n   - The closure returns a `View`, which is typically the view defined in the `body` property of the route. However, you can wrap this view with additional modifiers or environment objects as needed.\n\n#### When to Use `environmentInjection`\n\n- **Dependency Injection:** If your views need access to specific environment objects, you can inject them here. For instance, you might inject a data model or a service object that the view needs to function properly.\n\n- **Dynamic Modifications:** If certain views require dynamic modifications based on the route, you can apply those changes in this closure. This allows for greater flexibility and reuse of views.\n\n##### Example Scenario\n\nImagine you have a route that requires access to a user profile object, which is stored in an environment. You could inject this dependency directly through the `environmentInjection` closure:\n\n```swift\nenvironmentInjection: { route in\n  route.body\n    .environmentObject(UserProfile())\n}\n```\n\n### Navigation route methods\n\n`RoutingManager` provides several methods to manage navigation routing:\n\n| **Function** | **Explanation** | **Example** |\n|-------------|---------------|-----------|\n| `push(to screens: Route...) -\u003e NavigationResult` | Pushes one or more screens onto the navigation stack. | `push(to: homeScreen, detailsScreen)` |\n| `goBack(_ numberOfScreens: Int) -\u003e NavigationResult` | Navigates back by a specified number of screens. | `goBack(2)` |\n| `goToOccurrence(of screen: Route, direction: OccurrenceDirection) -\u003e NavigationResult` | Navigates to a specific occurrence of a screen in the navigation stack, based on direction. | `goToOccurrence(of: profileScreen, direction: .first)` |\n| `replaceCurrentScreen(with screen: Route) -\u003e NavigationResult` | Replaces the current screen with a new screen. | `replaceCurrentScreen(with: settingsScreen)` |\n| `replace(stack: Stack, with routes: [Route]) -\u003e NavigationResult` | Replaces a specific navigation stack with a new sequence of screens, while keeping other stacks unchanged. | `replace(stack: .productStack, with: homeScreen, productsScreen, itemScreen)` |\n| `replaceCurrentStack(with routes: [Route]) -\u003e NavigationResult` | Replaces the entire navigation stack associated with this manager with a new sequence of screens. | `replaceCurrentStack(with: homeScreen, profileScreen, settingsScreen)` |\n| `override(navigation: [Stack: [Route]]) -\u003e NavigationResult` | Overrides the entire stored navigation state with a new dictionary of stacks and routes. This completely replaces all stacks and routes. | `override(navigation: [.main: [homeScreen, cartScreen], .auth: [loginScreen]])` |\n| `resetNavigation() -\u003e NavigationResult` | Resets the navigation stack, removing all screens. | `resetNavigation()` |\n\n### Navigation stack methods\n\n`RoutingManager` provides several methods to manage navigation stacks:\n\n| **Function** | **Explanation** | **Example** |\n|-------------|---------------|-----------|\n| `listRoutes() -\u003e [Stack: [Route]]` | Retrieves a list of all routes currently present in the navigation stack. | `listRoutes()` |\n| `save() -\u003e NavigationResult` | Saves the current navigation state. | `save()` |\n| `load() -\u003e NavigationResult` | Loads a previously saved navigation state. | `load()` |\n| `delete() -\u003e NavigationResult` | Deletes the saved navigation state. | `delete()` |\n\n### Storage Options\n\n`RoutingManager` supports multiple storage options for saving and loading navigation paths:\n\n#### Memory storage\n\nIn-memory storage is the simplest form of storage. It stores navigation paths temporarily during the app's session and does not persist across app launches.\n\n```swift\n@State private var routeManager: Routes = .init(\n  storage: .memory,\n  stack: Stack.main,\n  for: Route.self\n)\n```\n\n#### JSON file storage\n\nJSON file storage saves navigation paths as `.json` files on the device, allowing them to persist across app launches. This is useful for apps that need to restore navigation states after a restart.\n\n```swift\n@State private var routeManager: Routes = .init(\n  storage: .json,\n  stack: Stack.main,\n  for: Route.self\n)\n```\n\n#### Custom Storage\n\nYou can implement your own storage by conforming to the `FileStorageRepresentable` protocol. For example, here's how you might implement XML storage:\n\n```swift\n\n// XMLFileStorage.swift\nclass XMLFileStorage\u003cT: Codable\u003e: FileStorageRepresentable {\n  private let fileManager = FileManager.default\n  private let fileURL: URL\n\n  init(fileName: String = \"NavigationState.xml\") {\n        let directory = FileManager.default\n            .urls(for: .documentDirectory, in: .userDomainMask)\n            .first!\n        self.fileURL = directory.appendingPathComponent(fileName)\n  }\n\n  func save(_ object: T) throws {\n    let encoder = PropertyListEncoder()\n    encoder.outputFormat = .xml\n    let data = try encoder.encode(object)\n    try data.write(to: fileURL)\n  }\n\n  func load() throws -\u003e T? {\n    guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }\n    let data = try Data(contentsOf: fileURL)\n    do {\n      let decoder = PropertyListDecoder()\n      return try decoder.decode(T.self, from: data)\n    } catch {\n      throw NavigationError.load(error)\n    }\n  }\n\n  func delete() throws {\n    guard FileManager.default.fileExists(atPath: fileURL.path) else { return }\n    try FileManager.default.removeItem(at: fileURL)\n  }\n}\n\n// SwiftUI view\n@State private var routeManager: Routes = .init(\n  storage: .custom(FileStorage(XMLFileStorage())),\n  stack: Stack.main,\n  for: Route.self\n)\n```\n\n### Error Handling\n\n`RoutingManager` provides robust error handling with the `.navigationError` modifier. This modifier allows you to handle errors that occur during navigation operations.\n\n```swift\nrouteManager.push(to: .home)\n  .navigationError { error in\n    print(\"An error occurred: \\(error.localizedDescription)\")\n  }\n```\n\n## Contributing\n\nContributions are welcome! If you have suggestions or improvements, please fork the repository and submit a pull request.\n\n## License\n\n`RoutingManager` is released under the MIT license. See LICENCE for details.\n\n[Shield1]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmarkbattistella%2FRoutingManager%2Fbadge%3Ftype%3Dswift-versions\n\n[Shield2]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmarkbattistella%2FRoutingManager%2Fbadge%3Ftype%3Dplatforms\n\n[Shield3]: https://img.shields.io/badge/Licence-MIT-white?labelColor=blue\u0026style=flat\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkbattistella%2Froutingmanager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkbattistella%2Froutingmanager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkbattistella%2Froutingmanager/lists"}