Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change StoreProtocol to implement transaction #47

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// swift-tools-version:5.5
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ObservableStore",
platforms: [.macOS(.v11), .iOS(.v15)],
platforms: [.macOS(.v11), .iOS(.v16)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
Expand Down
52 changes: 35 additions & 17 deletions Sources/ObservableStore/ObservableStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,20 @@ public protocol StoreProtocol {

var state: Model { get }

func send(_ action: Model.Action) -> Void
func transact(_ action: Model.Action) -> Void
}

extension StoreProtocol {
/// Send an action to the store to update state and generate effects.
/// Any effects generated are fed back into the store.
///
/// Note: SwiftUI requires that all UI changes happen on main thread.
/// `send(_:)` is run *asynchronously*.
nonisolated public func send(_ action: Model.Action) {
Task { @MainActor in
self.transact(action)
}
}
}

/// Store is a source of truth for a state.
Expand All @@ -206,15 +219,15 @@ where Model: ModelProtocol
{
/// Stores cancellables by ID
private(set) var cancellables: [UUID: AnyCancellable] = [:]

/// Private for all actions sent to the store.
private var _actions = PassthroughSubject<Model.Action, Never>()

/// Publisher for all actions sent to the store.
public var actions: AnyPublisher<Model.Action, Never> {
_actions.eraseToAnyPublisher()
}

/// Publisher for updates performed on state
private var _updates = PassthroughSubject<Model.UpdateType, Never>()

Expand Down Expand Up @@ -344,14 +357,19 @@ where Model: ModelProtocol
)
self.cancellables[id] = cancellable
}

/// Send an action to the store to update state and generate effects.
/// Any effects generated are fed back into the store.
///
/// Note: SwiftUI requires that all UI changes happen on main thread.
/// `send(_:)` is run *synchronously*. It is up to you to guarantee it is
/// run on main thread when SwiftUI is being used.
public func send(_ action: Model.Action) {
/// `store.transact(_:)` is run *synchronously*, but is not isolated to the
/// main actor. This is because many SwiftUI APIs like Binding are not
/// isolated to the main actor, yet require synchronous state change.
///
/// It is recommended you call `store.send(:)` which calls `.transact()`
/// *asynchronously* from a main actor isolated task. This will ensure
/// that all state change happens on the main thread.
public func transact(_ action: Model.Action) {
if loggingEnabled {
let actionString = String(describing: action)
logger.debug("Action: \(actionString, privacy: .public)")
Expand Down Expand Up @@ -413,26 +431,26 @@ public struct ViewStore<ViewModel: ModelProtocol>: StoreProtocol {
/// the value using a closure). Using the same approach as Binding
/// offers the most reliable results.
private var _get: () -> ViewModel
private var _send: (ViewModel.Action) -> Void
private var _transact: (ViewModel.Action) -> Void

/// Initialize a ViewStore from a `get` closure and a `send` closure.
/// These closures read from a parent store to provide a type-erased
/// view over the store that only exposes domain-specific
/// model and actions.
public init(
get: @escaping () -> ViewModel,
send: @escaping (ViewModel.Action) -> Void
transact: @escaping (ViewModel.Action) -> Void
) {
self._get = get
self._send = send
self._transact = transact
}

public var state: ViewModel {
self._get()
}

public func send(_ action: ViewModel.Action) {
self._send(action)
public func transact(_ action: ViewModel.Action) {
self._transact(action)
}
}

Expand All @@ -445,7 +463,7 @@ extension ViewStore {
) {
self.init(
get: { get(store.state) },
send: { action in store.send(tag(action)) }
transact: { action in store.transact(tag(action)) }
)
}
}
Expand Down Expand Up @@ -607,12 +625,12 @@ extension Binding {
/// - Returns a binding suitable for use in a vanilla SwiftUI view.
public init<Action>(
get: @escaping () -> Value,
send: @escaping (Action) -> Void,
transact: @escaping (Action) -> Void,
tag: @escaping (Value) -> Action
) {
self.init(
get: get,
set: { value in send(tag(value)) }
set: { value in transact(tag(value)) }
)
}
}
Expand All @@ -624,7 +642,7 @@ extension StoreProtocol {
) -> Binding<Value> {
Binding(
get: { get(self.state) },
set: { value in self.send(tag(value)) }
set: { value in self.transact(tag(value)) }
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/ObservableStoreTests/BindingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ final class BindingTests: XCTestCase {

let binding = Binding(
get: { store.state.text },
send: store.send,
transact: store.transact,
tag: Action.setText
)

Expand Down
10 changes: 5 additions & 5 deletions Tests/ObservableStoreTests/ComponentMappingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class ComponentMappingTests: XCTestCase {
)

let send = Address.forward(
send: store.send,
send: store.transact,
tag: ParentChildCursor.default.tag
)

Expand Down Expand Up @@ -161,8 +161,8 @@ class ComponentMappingTests: XCTestCase {
),
environment: ()
)
store.send(.keyedChild(action: .setText("BBB"), key: "a"))
store.send(.keyedChild(action: .setText("AAA"), key: "a"))
store.transact(.keyedChild(action: .setText("BBB"), key: "a"))
store.transact(.keyedChild(action: .setText("AAA"), key: "a"))

XCTAssertEqual(
store.state.keyedChildren["a"]?.text,
Expand Down Expand Up @@ -191,8 +191,8 @@ class ComponentMappingTests: XCTestCase {
state: ParentModel(),
environment: ()
)
store.send(.setText("Woo"))
store.send(.setText("Woo"))
store.transact(.setText("Woo"))
store.transact(.setText("Woo"))

XCTAssertEqual(
store.state.child.text,
Expand Down
28 changes: 14 additions & 14 deletions Tests/ObservableStoreTests/ObservableStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ final class ObservableStoreTests: XCTestCase {
environment: AppModel.Environment()
)

store.send(.increment)
store.transact(.increment)

XCTAssertEqual(store.state.count, 1, "state is advanced")
}

Expand Down Expand Up @@ -139,9 +139,9 @@ final class ObservableStoreTests: XCTestCase {
state: AppModel(),
environment: AppModel.Environment()
)
store.send(.createEmptyFxThatCompletesImmediately)
store.send(.createEmptyFxThatCompletesImmediately)
store.send(.createEmptyFxThatCompletesImmediately)
store.transact(.createEmptyFxThatCompletesImmediately)
store.transact(.createEmptyFxThatCompletesImmediately)
store.transact(.createEmptyFxThatCompletesImmediately)
let expectation = XCTestExpectation(
description: "cancellable removed when publisher completes"
)
Expand Down Expand Up @@ -221,11 +221,11 @@ final class ObservableStoreTests: XCTestCase {
})
.store(in: &cancellables)

store.send(.setCount(10))
store.send(.setCount(10))
store.send(.setCount(10))
store.send(.setCount(10))
store.transact(.setCount(10))
store.transact(.setCount(10))
store.transact(.setCount(10))
store.transact(.setCount(10))

let expectation = XCTestExpectation(
description: "publisher does not fire when state does not change"
)
Expand Down Expand Up @@ -357,10 +357,10 @@ final class ObservableStoreTests: XCTestCase {
})
.store(in: &cancellables)

store.send(.setCount(1))
store.send(.setCount(2))
store.send(.setCount(3))
store.transact(.setCount(1))
store.transact(.setCount(2))
store.transact(.setCount(3))

let expectation = XCTestExpectation(
description: "actions publisher fires for every action"
)
Expand Down
4 changes: 2 additions & 2 deletions Tests/ObservableStoreTests/ViewStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ final class ViewStoreTests: XCTestCase {
tag: ParentChildCursor.default.tag
)

viewStore.send(.setText("Foo"))
viewStore.transact(.setText("Foo"))

XCTAssertEqual(
store.state.child.text,
Expand All @@ -129,7 +129,7 @@ final class ViewStoreTests: XCTestCase {
tag: ParentChildCursor.default.tag
)

viewStore.send(.setText("Foo"))
viewStore.transact(.setText("Foo"))

XCTAssertEqual(
store.state.child.text,
Expand Down