Skip to content

Commit

Permalink
0.19.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Mar 11, 2024
1 parent b4faacd commit 9f75d61
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 68 deletions.
Binary file not shown.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Introduction

VDStore is a minimalistic iOS architecture library designed to manage application state in a clean and native manner. It provides a `Store` struct that enables state mutation, state subscription, di injection, and fragmentation into scopes for scaling. VDStore is compatible with both SwiftUI and UIKit.
VDStore is a minimalistic iOS architecture library designed to manage application state in a clean and native manner.
It provides a `Store` struct that enables state mutation, state subscription, di injection, and fragmentation into scopes for scaling.
VDStore is compatible with both SwiftUI and UIKit.

## Features

Expand Down Expand Up @@ -50,7 +52,8 @@ struct CounterView: View {
}
}
```
`ViewStore` is a property wrapper that automatically subscribes to state changes and updates the view. `ViewStore` can be initialized with either `Store` or `State` instances.
`ViewStore` is a property wrapper that automatically subscribes to state changes and updates the view.
`ViewStore` can be initialized with either `Store` or `State` instances.

### Using with UIKit

Expand Down Expand Up @@ -78,7 +81,10 @@ final class CounterViewController: UIViewController {
### Defining actions
You can edit the state in any way you prefer, but the simplest one is extending Store.

There is a helper macro called `@Actions`. `@Actions` redirect all your methods calls through your custom middlewares that allows you to intrecept all calls in runtime. For example, you can use it to log all calls or state changes. Also `@Actions` make all your `async` methods cancellable.
There is a helper macro called `@Actions`.
`@Actions` redirect all your methods calls through your custom middlewares that allows you to intrecept all calls in runtime.
For example, you can use it to log all calls or state changes.
Also `@Actions` make all your `async` methods cancellable.
```swift
@Actions
extension Store<Converter> {
Expand Down Expand Up @@ -158,7 +164,7 @@ import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/VDStore.git", from: "0.18.0")
.package(url: "https://github.com/dankinsoid/VDStore.git", from: "0.19.0")
],
targets: [
.target(name: "SomeProject", dependencies: ["VDStore"])
Expand Down
84 changes: 61 additions & 23 deletions Sources/VDStore/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import Foundation
///
/// ### Scoping
///
/// The most important operation defined on ``Store`` is the ``scope(get:set:)`` or ``scope(_ keyPayh:)`` method,
/// The most important operation defined on ``Store`` is the ``scope(get:set:)`` or ``scope(_ keyPayh:)`` methods,
/// which allows you to transform a store into one that deals with child state. This is
/// necessary for passing stores to subviews that only care about a small portion of the entire
/// application's domain.
/// The store supports dynamic member lookup so that you can scope with a specific field in the state.
///
/// For example, if an application has a tab view at its root with tabs for activity, search, and
/// profile, then we can model the domain like this:
Expand Down Expand Up @@ -61,17 +62,17 @@ import Foundation
/// var body: some View {
/// TabView {
/// ActivityView(
/// $state.scope(\.activity)
/// $state.activity
/// )
/// .tabItem { Text("Activity") }
///
/// SearchView(
/// $state.scope(\.search)
/// $state.search
/// )
/// .tabItem { Text("Search") }
///
/// ProfileView(
/// $state.scope(\.profile)
/// $state.profile
/// )
/// .tabItem { Text("Profile") }
/// }
Expand All @@ -84,17 +85,13 @@ import Foundation
/// The `Store` class is isolated to main thread by @MainActor attribute.
@MainActor
@propertyWrapper
@dynamicMemberLookup
public struct Store<State> {

/// The state of the store.
public var state: State {
get { box.state }
nonmutating set {
if suspendAllSyncStoreUpdates, !box.isUpdating {
suspendSyncUpdates()
}
box.state = newValue
}
nonmutating set { box.state = newValue }
}

/// Injected dependencies.
Expand All @@ -104,8 +101,7 @@ public struct Store<State> {

/// A publisher that emits when state changes.
///
/// This publisher supports dynamic member lookup so that you can pluck out a specific field in
/// the state:
/// This publisher supports dynamic member lookup so that you can pluck out a specific field in the state:
///
/// ```swift
/// store.publisher.alert
Expand Down Expand Up @@ -171,7 +167,11 @@ public struct Store<State> {
/// // Construct a login view by scoping the store
/// // to one that works with only login domain.
/// LoginView(
/// store.scope(state: \.login)
/// store.scope {
/// $0.login
/// } set: {
/// $0.login = $1
/// }
/// )
/// ```
///
Expand Down Expand Up @@ -217,7 +217,7 @@ public struct Store<State> {
/// `LoginView` could be extracted to a module that has no access to `AppFeature`.
///
/// - Parameters:
/// - state: A writable key path from `State` to `ChildState`.
/// - keyPath: A writable key path from `State` to `ChildState`.
/// - Returns: A new store with its state transformed.
public func scope<ChildState>(_ keyPath: WritableKeyPath<State, ChildState>) -> Store<ChildState> {
scope {
Expand All @@ -227,6 +227,39 @@ public struct Store<State> {
}
}

/// Scopes the store to one that exposes child state.
///
/// This can be useful for deriving new stores to hand to child views in an application. For
/// example:
///
/// ```swift
/// struct AppFeature {
/// var login: Login.State
/// // ...
/// }
///
/// // A store that runs the entire application.
/// let store = Store(AppFeature())
///
/// // Construct a login view by scoping the store
/// // to one that works with only login domain.
/// LoginView(
/// store.login
/// )
/// ```
///
/// Scoping in this fashion allows you to better modularize your application. In this case,
/// `LoginView` could be extracted to a module that has no access to `AppFeature`.
///
/// - Parameters:
/// - keyPath: A writable key path from `State` to `ChildState`.
/// - Returns: A new store with its state transformed.
public subscript<ChildState>(
dynamicMember keyPath: WritableKeyPath<State, ChildState>
) -> Store<ChildState> {
scope(keyPath)
}

/// Injects the given value into the store's.
/// - Parameters:
/// - keyPath: A key path to the value in the store's dependencies.
Expand Down Expand Up @@ -265,19 +298,24 @@ public struct Store<State> {

/// Suspends the store from updating the UI until the block returns.
public func update<T>(_ update: () throws -> T) rethrows -> T {
if !suspendAllSyncStoreUpdates, !box.isUpdating {
defer { box.afterUpdate() }
box.beforeUpdate()
}
defer { box.afterUpdate() }
box.beforeUpdate()
let result = try update()
return result
}
}

/// Suspends the store from updating the UI while all synchronous operations are being performed.
public func suspendSyncUpdates() {
box.beforeUpdate()
DispatchQueue.main.async { [box] in
box.afterUpdate()
public extension Store where State: MutableCollection {

subscript(_ index: State.Index) -> Store<State.Element> {
scope(index)
}

func scope(_ index: State.Index) -> Store<State.Element> {
scope {
$0[index]
} set: {
$0[index] = $1
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions Sources/VDStore/StoreExtensions/Iflet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

public extension Store {

func or<T>(_ defaultValue: @escaping @autoclosure () -> T) -> Store<T> where T? == State {
scope {
$0 ?? defaultValue()
} set: {
$0 = $1
}
}

func onChange<V>(
of keyPath: WritableKeyPath<State, V>,
removeDuplicates isDuplicate: @escaping (V, V) -> Bool,
_ operation: @MainActor @escaping (_ oldValue: V, _ newValue: V, inout State) -> Void
) -> Store {
scope {
$0
} set: {
let oldValue = $0[keyPath: keyPath]
$0 = $1
operation(oldValue, $1[keyPath: keyPath], &$0)
}
}

func onChange<V: Equatable>(
of keyPath: WritableKeyPath<State, V>,
_ operation: @MainActor @escaping (_ oldValue: V, _ newValue: V, inout State) -> Void
) -> Store {
onChange(of: keyPath, removeDuplicates: ==, operation)
}
}

public extension Store where State: MutableCollection {

func forEach(_ operation: @MainActor (Store<State.Element>) throws -> Void) rethrows {
for index in state.indices {
try operation(self[index])
}
}

func forEach(_ operation: @MainActor (Store<State.Element>) async throws -> Void) async rethrows {
for index in state.indices {
try await operation(self[index])
}
}
}
Loading

0 comments on commit 9f75d61

Please sign in to comment.