{"id":30719859,"url":"https://github.com/dankinsoid/vdstore","last_synced_at":"2025-09-03T10:42:30.755Z","repository":{"id":189129042,"uuid":"680098480","full_name":"dankinsoid/VDStore","owner":"dankinsoid","description":"Simple store-based architecture for iOS propjects.","archived":false,"fork":false,"pushed_at":"2025-07-03T11:02:09.000Z","size":1821,"stargazers_count":1,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-03T12:19:51.820Z","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,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"dankinsoid","open_collective":"voidilov-daniil","ko_fi":"dankinsoid","custom":["https://paypal.me/voidilovuae"]}},"created_at":"2023-08-18T10:37:26.000Z","updated_at":"2025-07-03T11:02:05.000Z","dependencies_parsed_at":"2023-08-18T12:21:24.618Z","dependency_job_id":"cb10d3f7-6032-42f9-b7a8-383a4f7fe741","html_url":"https://github.com/dankinsoid/VDStore","commit_stats":null,"previous_names":["dankinsoid/vdstore"],"tags_count":45,"template":false,"template_full_name":"dankinsoid/iOSLibraryTemplate","purl":"pkg:github/dankinsoid/VDStore","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDStore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDStore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDStore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDStore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dankinsoid","download_url":"https://codeload.github.com/dankinsoid/VDStore/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dankinsoid%2FVDStore/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273431361,"owners_count":25104491,"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","status":"online","status_checked_at":"2025-09-03T02:00:09.631Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":"2025-09-03T10:42:29.774Z","updated_at":"2025-09-03T10:42:30.736Z","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":[],"sub_categories":[],"readme":"# VDStore\n\n## Introduction\n\nVDStore is a minimalistic iOS architecture library designed to manage application state in a clean and native manner.\nIt provides a `Store` struct that enables state mutation, state subscription, di injection, and fragmentation into scopes for scaling.\nVDStore is compatible with both SwiftUI and UIKit.\n\n## Features\n\n- **State Management**: Easily handle and mutate the state of your app in a structured and type-safe way.\n- **State Subscription**: Observe state changes and update your UI in a reactive manner.\n- **Dependencies Injection**: Seamlessly manage dependencies and inject services as needed.\n- **Fragmentation into Scopes**: Efficiently break down and manage complex states by creating focused sub-stores with scoped functionality.\n- **Non-mutating Properties**: Support for class-based states and fine-grained control over which property changes trigger UI updates.\n\n## Usage\n\n### Basic Example\n\nHere's how you can define a simple counter state and its mutations:\n\n```swift\nstruct Counter: Equatable {\n  var counter: Int = 0\n}\n\nextension Store\u003cCounter\u003e {\n\n  func add() {\n    state.counter += 1\n  }\n}\n```\n\n### Using with SwiftUI\n\nExample of integrating `VDStore` with a SwiftUI `View`:\n\n```swift\nstruct CounterView: View {\n\n  @ViewStore var counter = Counter() \n\n  var body: some View {\n    HStack {\n      Text(\"\\(counter.counter)\")\n      Button(\"Add\") {\n         $counter.add()\n      }\n      SomeChildView($counter)\n    }\n  }\n}\n```\n`ViewStore` is a property wrapper that automatically subscribes to state changes and updates the view.\n`ViewStore` can be initialized with either `Store` or `State` instances.\n\n### Using with UIKit\n\nExample of integrating `VDStore` with a `UIViewController`:\n\n```swift\nfinal class CounterViewController: UIViewController {\n\n  @Store var state = Counter()\n  private var cancellableSet: Set\u003cAnyCancellable\u003e = []\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n    $state.publisher.sink { [weak self] state in\n      self?.render(with: state)\n    }\n    .store(in: \u0026cancellableSet)\n  }\n\n  func tapAddButton() {\n    $state.add()\n  }\n}\n```\n\n### Defining actions\nYou can edit the state in any way you prefer, but the simplest one is extending Store.\n\nThere is a helper macro called `@Actions`.\n`@Actions` redirect all your methods calls through your custom middlewares that allows you to intrecept all calls in runtime.\nFor example, you can use it to log all calls or state changes.\nAlso `@Actions` make all your `async` methods cancellable.\n```swift\n@Actions\nextension Store\u003cConverter\u003e {\n\n  @CancelInFlight\n  func updateRates() async {\n    state.isLoading = true\n    defer { state.isLoading = false }\n    do {\n      try await di.api.updateRates()\n      guard !Task.isCancelled else { return }\n      ...\n    } catch {\n      ...\n    }\n  }\n}\n\n```\n\n### Adding Dependencies\n\nTo define a dependency you should extend `DIValues` with a computed property like this:\n```swift\nextension DIValues {\n\n   public var someService: SomeService {\n      get { self[\\.someService] ?? SomeService.shared }\n      set { self[\\.someService] = newValue }\n   }\n}\n```\nOr you can use on of two macros:\n```swift\nextension DIValues {\n\n   @DI\n   public var someService: SomeService = .shared\n}\n```\n```swift\n@DIValuesList\nextension DIValues {\n\n   public var someService1: SomeService1 = .shared\n   public var someService2: SomeService2 = .shared\n}\n```\nTo inject a dependency you should use `di` method:\n```swift\nfunc getSomeChildStore(store: Store\u003cCounter\u003e) -\u003e Store\u003cInt\u003e {\n   store\n     .scope(\\.counter)\n     .di(\\.someService, SomeService())\n}\n```\nTo use a dependency you should use `di` property:\n```swift\nstore.di.someService.someMethod()\n```\nThere is `valueFor` global method that allows you to define default values depending on the environment: live, test or preview.\n```swift\nextension DIValues {\n\n  @DI\n  public var someService: SomeService = valueFor(\n\tlive: SomeService.shared,\n\ttest: SomeServiceMock()\n)\n```\n\n### Non-mutating Properties\n\nVDStore provides fine-grained control over which property changes trigger store updates using Swift's native value semantics. The mechanism is simple: **only state mutations trigger updates**. Make a substate non-mutating in any way, and updates will only be available when scoping to that specific substate.\n\n#### The Simple Mechanism\n\nThere are two main ways to achieve non-mutating substates:\n\n1. **Use a class** - Class properties don't trigger parent updates when modified\n2. **Use `@NonMutatingSet`** - A property wrapper that makes specific struct properties non-mutating\n\n#### Screen-based Architecture\n\nConsider a typical app with multiple screens. You can structure your global state so that updates to one screen don't trigger rebuilds for other screens:\n\n```swift\nstruct AppState {\n  @NonMutatingSet var homeScreen: HomeScreenState = HomeScreenState()\n  @NonMutatingSet var profileScreen: ProfileScreenState = ProfileScreenState()\n  @NonMutatingSet var settingsScreen: SettingsScreenState = SettingsScreenState()\n  \n  // Global app data that affects all screens\n  var user: User? = nil\n  var isOnline: Bool = true\n}\n\nstruct HomeScreenState {\n  var posts: [Post] = []\n  var isLoading: Bool = false\n  var searchQuery: String = \"\"\n}\n\nstruct ProfileScreenState {\n  var userProfile: UserProfile? = nil\n  var isEditing: Bool = false\n  var avatarImage: UIImage? = nil\n  @NonMutatingSet var recentActivities: [Activity] = []\n}\n\nstruct SettingsScreenState {\n  var theme: Theme = .light\n  var notificationsEnabled: Bool = true\n  var selectedLanguage: String = \"en\"\n}\n```\n\n#### Independent Screen Updates\n\nEach screen gets its own scoped store that only triggers updates for that specific screen:\n\n```swift\nstruct HomeView: View {\n  @ViewStore var homeState: HomeScreenState\n  \n  init(_ store: Store\u003cAppState\u003e) {\n    _homeState = ViewStore(store.homeScreen)\n  }\n  \n  var body: some View {\n    VStack {\n      if homeState.isLoading {\n        ProgressView()\n      }\n      \n      List(homeState.posts) { post in\n        PostRow(post: post)\n      }\n      \n      Button(\"Load Posts\") {\n        $homeState.loadPosts()\n      }\n    }\n  }\n}\n\nstruct ProfileView: View {\n  @ViewStore var profileState: ProfileScreenState\n  \n  init(_ store: Store\u003cAppState\u003e) {\n    _profileState = ViewStore(store.profileScreen)\n  }\n  \n  var body: some View {\n    VStack {\n      if let profile = profileState.userProfile {\n        ProfileCard(profile: profile)\n      }\n      \n      Button(\"Edit Profile\") {\n        $profileState.startEditing()\n      }\n    }\n  }\n}\n```\n\n#### How It Works\n\nThe magic is in Swift's native value semantics:\n\n```swift\nextension Store\u003cHomeScreenState\u003e {\n  func loadPosts() async {\n    state.isLoading = true  // Only HomeView rebuilds\n    // ... fetch posts\n    state.posts = newPosts  // Only HomeView rebuilds\n    state.isLoading = false // Only HomeView rebuilds\n  }\n}\n\nextension Store\u003cProfileScreenState\u003e {\n  func startEditing() {\n    state.isEditing = true  // Only ProfileView rebuilds\n  }\n}\n\nextension Store\u003cAppState\u003e {\n  func setUser(_ user: User) {\n    state.user = user  // All views rebuild (global state change)\n  }\n  \n  func updateHomeScreenDirectly() {\n    // This WON'T trigger any updates because homeScreen is non-mutating\n    state.homeScreen.posts.append(newPost)\n    \n    // To trigger updates, you need to scope to the substate:\n    // homeStore.state.posts.append(newPost)  // This WILL trigger updates\n  }\n}\n```\n\n**Key insight**: When a property is non-mutating, changing it doesn't mutate the parent struct, so no updates are triggered at the parent level. Updates only happen when you scope directly to that substate.\n\n#### Shared Dependencies\n\nAll screen stores share the same dependency injection context:\n\n```swift\n// All screens can access the same services\nhomeStore.di.apiService.fetchPosts()\nprofileStore.di.apiService.updateProfile(...)\nsettingsStore.di.userDefaults.save(...)\n```\n\nThis approach provides optimal performance by ensuring that state changes in one screen don't cause unnecessary re-renders in other screens, while maintaining a unified global state and shared dependency context.\n\n#### Manual Update Control\n\nFor ultimate control over when updates are triggered, you can use the `update()` methods:\n\n```swift\nextension Store\u003cAppState\u003e {\n  func performBatchOperations() {\n    // Multiple changes without triggering updates\n    state.homeScreen.posts.append(\"Post 1\")\n    state.homeScreen.posts.append(\"Post 2\") \n    state.profileScreen.userName = \"John\"\n    \n    // Manually trigger a single update for all changes\n    update()\n  }\n}\n```\n\nYou can also use classes exclusively for your state and trigger all updates manually, giving you complete control:\n\n```swift\nclass AppState {\n  var counter: Int = 0\n  var data: [String] = []\n}\n\nextension Store\u003cAppState\u003e {\n  func incrementAndAddData() {\n    // These changes won't trigger any updates\n    state.counter += 1\n    state.data.append(\"New item\")\n    \n    // Only trigger update when you want it\n    update()\n  }\n}\n\n## Requirements\n\n- Swift 5.7+\n- iOS 13.0+\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.7\nimport PackageDescription\n\nlet package = Package(\n  name: \"SomeProject\",\n  dependencies: [\n    .package(url: \"https://github.com/dankinsoid/VDStore.git\", from: \"0.37.0\")\n  ],\n  targets: [\n    .target(name: \"SomeProject\", dependencies: [\"VDStore\"])\n  ]\n)\n```\n```ruby\n$ swift build\n```\n\n## Author\n\ndankinsoid, voidilov@gmail.com\n\n## License\n\nVDStore 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%2Fvdstore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdankinsoid%2Fvdstore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdankinsoid%2Fvdstore/lists"}