{"id":13871998,"url":"https://github.com/dankinsoid/VDFlow","last_synced_at":"2025-07-16T01:32:34.507Z","repository":{"id":56925569,"uuid":"311466781","full_name":"dankinsoid/VDFlow","owner":"dankinsoid","description":null,"archived":false,"fork":false,"pushed_at":"2024-09-21T09:31:19.000Z","size":162935,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-11-09T19:03:36.887Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/dankinsoid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"dankinsoid","open_collective":"voidilov-daniil","ko_fi":"dankinsoid","custom":["https://paypal.me/voidilovuae"]}},"created_at":"2020-11-09T21:15:01.000Z","updated_at":"2024-09-21T09:31:23.000Z","dependencies_parsed_at":"2024-03-17T01:17:07.964Z","dependency_job_id":"8e50e615-639b-48d0-ab98-8479b6fc513a","html_url":"https://github.com/dankinsoid/VDFlow","commit_stats":{"total_commits":195,"total_committers":3,"mean_commits":65.0,"dds":"0.46153846153846156","last_synced_commit":"704423369f43fcd19f37c0c7b55dee6f1d1dfbb1"},"previous_names":[],"tags_count":165,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDFlow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDFlow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDFlow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDFlow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dankinsoid","download_url":"https://codeload.github.com/dankinsoid/VDFlow/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226090030,"owners_count":17572114,"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-08-05T23:00:32.052Z","updated_at":"2025-07-16T01:32:34.497Z","avatar_url":"https://github.com/dankinsoid.png","language":"Swift","funding_links":["https://github.com/sponsors/dankinsoid","https://opencollective.com/voidilov-daniil","https://ko-fi.com/dankinsoid","https://paypal.me/voidilovuae"],"categories":["Swift"],"sub_categories":[],"readme":"# VDFlow\n\n## Description\nThis repository provides a new simple way to describe routers.\\\nI view the application flow as a tree of all possible screen states. From this point of view, navigation is the selection of a node of this tree.\n\n## Example\nTake for example an application with such a hierarchy of screens:\n```swift\n             TabView          \n   ┌────────────┼────────────┐\n  Home        Explore    NavigationView\n                    ┌────────┴────────┐\n                ProfileView       DetailView\n                                      │\n                                  ThemeSelector\n                              ┌───────┴───────┐\n                            Light           Dark\n```\n`ThemeSelector` is here to demonstrate that navigation can mean not only changing screens, but also changing any state of any view.\n\nDescribe your flow as a struct with `Step` properties:\n```swift\n@Steps\nstruct AppSteps {\n\n  var home\n  var explore = ExploreData()\n  var profile: ProfileSteps = .main\n  var none\n}\n\n@Steps\nstruct ProfileSteps {\n\n  var main\n  var detail: ThemeSteps = .none\n}\n\n@Steps\nstruct ThemeSteps {\n\n  var light\n  var dark\n  var none\n}\n```\n```swift\nvar steps: AppSteps = .home\n```\nIf you want to open `Explore` you need mark `explore` as selected. You have several ways to do it:\n1. Set `selected` property:\n```swift\nsteps.selected = .explore\n```\n2. Use auto-generated static functions:\n```swift\nsteps = .explore(ExploreData())\n```\nYou can check which property is selected:\n1. With `selected` property:\n```swift\n$steps.selected == .explore\n```\nAlso you can set initial selected property:\n```swift\nvar profileFlow: ProfileSteps = .main\n```\n### Deeplink\n Then you got a deep link for example and you need to navigate to the `Profile` tab, push to `DetailView` and select `Dark` theme in `ThemeSelector`.\n ```swift\n steps.profile.$detail.select(with: .dark)\n ```\n Now `profile`, `detail`, `dark` properties are marked as selected.\n\n### Integration with UI\nSwiftUI is a state driven framework, so it's easy to implement navigation with `Step`s.\n\n#### 1. `StateStep` property wrapper.\n`StateStep` updates view, stores your flow struct or binds it from parent view as an environment value. To bind flow down the view hierarchy you need use `.step(...)` or `.stepEnvironment(...)` view modifiers or initialize `StateStep` with `Binding\u003cStep\u003c...\u003e\u003e`.\\\n`stepEnvironment` binds current step down the view hierarchy for embedded `StateStep` properties.\n`step` modifier is just a combination of `tag` and `stepEnvironment` modifiers.\n```swift\nstruct MainTabView: View {\n\n  @StateStep var step: AppSteps = .home\n  \n  var body: some View {\n    TabView(selection: $step.selected) {\n      HomeView()\n        .step(_step.$home)\n      \n      ExploreView()\n        .step(_step.$explore)\n      \n      ProfileNavigation()\n        .step(_step.$profile)\n    }\n    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))\n  }\n}\n\nstruct ProfileNavigation: View {\n  \n  @StateStep var step = ProfileSteps()\n  \n  var body: some View {\n    NavigationView {\n      ProfileView {\n        NavigationLink(isActive: $step.isSelected(.detail)) {\n          ThemeSelectorView()\n            .stepEnvironment($step.$detail)\n        } label: {\n          Text(\"Change Theme\")\n        }\n      }\n    }\n  }\n}\n\nstruct ThemeSelectorView: View {\n  \n  @StateStep var step = ThemeSteps()\n  \n  var body: some View {\n    Picker(\"Theme\", selection: $step.selected) {\n      Text(\"Light Mode\")\n        .tag(ThemeSteps.Steps.light)\n      \n      Text(\"Dark Mode\")\n        .tag(ThemeSteps.Steps.dark)\n    }\n    .pickerStyle(WheelPickerStyle())\n  }\n}\n```\n#### 2. Binding\nYou can use `Step` directly without `StateStep` wrapper, in `ObservableObject` view model or as a part of state in [TCA](https://github.com/pointfreeco/swift-composable-architecture) `Store`, etc.\n\n#### 3. UIKit\nThere is no any special instrument for UIKit, because UIKit doesn't support state driven navigation, but it's possible to use Combine to subscribe on `Step` changes:\n```swift\nlet stepsSubject = CurrentValueSubject(AppSteps(.home))\n\nstepsSubject\n  .map(\\.selected)\n  .removeDublicates()\n  .sink { selected in\n    switch selected {\n    case .home:\n      // Handle home tab selection\n    case .explore:\n      // Handle explore tab selection\n    case .profile:\n      // Handle profile tab selection\n    default:\n      break\n    }\n  }\n\nstepsSubject.value.$explore.select()\n```\nor use `didSet`:\n```swift\nvar steps = AppSteps(.home) {\n  didSet {\n    guard oldValue.selected != steps.selected else { return }\n    // Handle selection change\n    ... \n  }\n}\n```\n\n### Observing Steps\n\nVDFlow provides a built-in observer system to track step changes throughout your application. This is useful for analytics, logging, or triggering side effects when navigation occurs.\n\n```swift\n// Create a custom observer\nclass MyStepsObserver: StepsObserver {\n  func stepWillChange\u003cParent: StepsCollection, Value\u003e(\n    to newValue: Parent.AllSteps, \n    in type: Parent.Type, \n    with value: Value\n  ) {\n    print(\"Will navigate to \\(newValue) in \\(Parent.self)\")\n  }\n  \n  func stepDidChange\u003cParent: StepsCollection, Value\u003e(\n    to newValue: Parent.AllSteps, \n    in type: Parent.Type, \n    with value: Value\n  ) {\n    print(\"Did navigate to \\(newValue) in \\(Parent.self)\")\n    \n    // Perform heavy work in background queue to avoid blocking the UI\n    DispatchQueue.global().async {\n      // Analytics tracking, logging, etc.\n    }\n  }\n}\n\n// Register the observer globally\nStepSystem.observer = MyStepsObserver()\n```\n\nThe observer will be called whenever any step changes in the application, allowing for centralized navigation tracking.\n\n### Tools\n\n#### `NavigationLink` convenience init\n```swift\n@StateStep var steps = ProfileSteps()\n...\nNavigationLink(step: _steps.$detail) {\n  ThemeSelectorView()\n} label: {\n  Text(\"Change Theme\")\n}\n```\n\n#### `navigationPath()` extension on `Binding\u003cStep\u003c...\u003e\u003e` and two `navigationDestination` methods\n```swift\n@StateStep var steps = ProfileSteps()\n    \nvar body: some View {\n    NavigationStack(path: $steps.navigationPath) {\n        ProfileView()\n            .navigationDestination(step: _steps.$detail) {\n                ThemeSelectorView()\n            }\n            // or\n            .navigationDestination(for: _steps) {\n                switch $0 {\n                case .detail:\n                    ThemeSelectorView()\n                    \t.step(_steps.$detail)\n                default:\n                    EmptyView()\n                }\n            }\n    }\n    \n}\n```\n## Installation\n\n1. [Swift Package Manager](https://github.com/apple/swift-package-manager)\n\nCreate a `Package.swift` file.\n```swift\n// swift-tools-version:5.9\nimport PackageDescription\n\nlet package = Package(\n  name: \"SomeProject\",\n  dependencies: [\n    .package(url: \"https://github.com/dankinsoid/VDFlow.git\", from: \"4.32.0\")\n  ],\n  targets: [\n    .target(name: \"SomeProject\", dependencies: [\"VDFlow\"])\n  ]\n)\n```\n```ruby\n$ swift build\n```\n## Author\n\nDaniil Voidilov, voidilov@gmail.com\n\n## License\n\nVDFlow 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%2Fdankinsoid%2FVDFlow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdankinsoid%2FVDFlow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdankinsoid%2FVDFlow/lists"}