Skip to content

Commit 9473b2b

Browse files
authored
Merge pull request #22 from anthony1810/update-readme-1.1.0
Update README for 1.1.0 API changes
2 parents 99d5e1f + 61d864a commit 9473b2b

1 file changed

Lines changed: 48 additions & 51 deletions

File tree

README.md

Lines changed: 48 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Add the following to your `Package.swift`:
4747

4848
```swift
4949
dependencies: [
50-
.package(url: "https://github.com/anthony1810/ScreenStateKit.git", from: "1.0.0")
50+
.package(url: "https://github.com/anthony1810/ScreenStateKit.git", from: "1.1.0")
5151
]
5252
```
5353

@@ -107,16 +107,16 @@ actor FeatureViewStore: ScreenActionStore {
107107
private let dataService: DataServiceProtocol
108108

109109
// MARK: - State Management
110-
private let actionLocker = ActionLocker()
111-
weak var viewState: FeatureViewState?
110+
private let actionLocker = ActionLocker.nonIsolated
111+
private(set) weak var viewState: FeatureViewState?
112112

113113
// MARK: - Init
114114
init(dataService: DataServiceProtocol) {
115115
self.dataService = dataService
116116
}
117117

118118
// MARK: - Actions
119-
enum Action: ActionLockable, LoadingTrackable, Sendable {
119+
enum Action: ActionLockable, LoadingTrackable, Hashable {
120120
case fetchItems
121121
case loadMore
122122

@@ -135,32 +135,17 @@ actor FeatureViewStore: ScreenActionStore {
135135
self.viewState = state
136136
}
137137

138-
nonisolated func receive(action: Action) {
139-
Task {
140-
await isolatedReceive(action: action)
141-
}
142-
}
143-
144138
// MARK: - Action Processing
145-
func isolatedReceive(action: Action) async {
146-
guard await actionLocker.canExecute(action) else { return }
147-
await viewState?.loadingStarted(action: action)
148-
149-
do {
150-
switch action {
151-
case .fetchItems:
152-
try await fetchItems()
153-
case .loadMore:
154-
try await loadMoreItems()
155-
}
156-
} catch {
157-
await viewState?.showError(
158-
DisplayableError(message: error.localizedDescription)
159-
)
139+
func receive(action: Action) async throws {
140+
guard actionLocker.canExecute(action) else { return }
141+
defer { actionLocker.unlock(action) }
142+
143+
switch action {
144+
case .fetchItems:
145+
try await fetchItems()
146+
case .loadMore:
147+
try await loadMoreItems()
160148
}
161-
162-
await actionLocker.unlock(action)
163-
await viewState?.loadingFinished(action: action)
164149
}
165150

166151
// MARK: - Action Implementations
@@ -181,6 +166,8 @@ actor FeatureViewStore: ScreenActionStore {
181166
}
182167
```
183168

169+
> **How it works:** You only write business logic in `receive(action:)` and `throw` on errors. The framework's `dispatch` method (called by `nonisolatedReceive`) automatically handles `loadingStarted`, `loadingFinished`, and error routing to `viewState?.showError()`. No boilerplate needed.
170+
184171
**Action Flow:** Here's how actions are processed through `ActionLocker` and `LoadingTrackable`:
185172

186173
<p align="center">
@@ -219,7 +206,7 @@ struct FeatureView: View {
219206
await viewStore.binding(state: viewState)
220207

221208
// Initial data fetch
222-
viewStore.receive(action: .fetchItems)
209+
viewStore.nonisolatedReceive(action: .fetchItems)
223210
}
224211
}
225212

@@ -245,7 +232,7 @@ struct FeatureView: View {
245232
}
246233
}
247234
.refreshable {
248-
try? await viewStore.isolatedReceive(action: .fetchItems)
235+
try? await viewStore.receive(action: .fetchItems)
249236
}
250237
}
251238

@@ -457,7 +444,7 @@ final class ListViewState: LoadmoreScreenState, StateUpdatable {
457444
**Methods:**
458445
- `canExecuteLoadmore()` - Enables the load more indicator (no-op if `didLoadAllData` is true)
459446
- `updateDidLoadAllData(_ didLoadAllData: Bool)` - Updates the `didLoadAllData` flag and toggles `canShowLoadmore`
460-
- `ternimateLoadmoreView()` - Hides the load more indicator
447+
- `terminateLoadMoreView()` - Hides the load more indicator
461448

462449
### RMLoadmoreView
463450

@@ -511,13 +498,13 @@ struct ItemListView: View {
511498
}
512499
// Parent sets callbacks for child views to trigger
513500
.onCreated { [weak viewModel] in
514-
viewModel?.receive(action: .refreshItems)
501+
viewModel?.nonisolatedReceive(action: .refreshItems)
515502
}
516503
.onEdited { [weak viewModel] in
517-
viewModel?.receive(action: .refreshItems)
504+
viewModel?.nonisolatedReceive(action: .refreshItems)
518505
}
519506
.onDeleted { [weak viewModel] in
520-
viewModel?.receive(action: .refreshItems)
507+
viewModel?.nonisolatedReceive(action: .refreshItems)
521508
}
522509
}
523510
}
@@ -661,23 +648,19 @@ let producer = StreamProducer<Int>(element: 0, withLatest: true)
661648
let producer = StreamProducer<Int>(withLatest: false)
662649
```
663650

664-
**Non-isolated methods** for use from `nonisolated` or `deinit` contexts:
651+
**Non-isolated methods** for use from `nonisolated` contexts:
665652
- `nonIsolatedEmit(_ element:)` - Emits from a non-isolated context
666-
- `nonIsolatedFinish()` - Finishes the stream from a non-isolated context
653+
- `nonIsolatedFinish()` - *(Deprecated)* Streams are automatically finished when the producer is deallocated
667654

668655
### CancelBag
669656

670-
Manages and cancels multiple async tasks. Essential for cleanup in actors and view models.
657+
Manages and cancels multiple async tasks. Completed tasks are automatically removed from the bag. All remaining tasks are cancelled when the bag is deallocated.
671658

672659
```swift
673660
actor MyViewModel {
674-
private let cancelBag = CancelBag()
661+
private let cancelBag = CancelBag(onDuplicate: .cancelExisting)
675662
private let eventProducer = StreamProducer<DataEvent>()
676663

677-
deinit {
678-
cancelBag.cancelAllInTask()
679-
}
680-
681664
func startObserving() {
682665
// Store task with identifier for later cancellation
683666
Task.detached { [weak self] in
@@ -694,14 +677,26 @@ actor MyViewModel {
694677
}
695678
```
696679

680+
**Init — `DuplicatePolicy`:**
681+
- `CancelBag(onDuplicate: .cancelExisting)` - When a new task is stored with the same identifier, cancel the existing one
682+
- `CancelBag(onDuplicate: .cancelNew)` - When a new task is stored with the same identifier, cancel the new one
683+
684+
**Properties:**
685+
- `isEmpty: Bool` - Whether the bag has no running tasks
686+
- `count: Int` - Number of running tasks
687+
697688
**Methods:**
698689
- `cancelAll()` - Cancels all stored tasks
699-
- `cancel(forIdentifier:)` - Cancels a specific task by its identifier
700-
- `cancelAllInTask()` - Non-isolated version for use in `deinit`
690+
- `cancel(forIdentifier:)` - Cancels a specific task by its identifier (accepts `AnyHashable`)
701691

702692
**Task extension:**
703-
- `task.store(in: cancelBag)` - Store with auto-generated identifier
704-
- `task.store(in: cancelBag, withIdentifier: "id")` - Store with a specific identifier (cancels any existing task with the same identifier)
693+
- `task.store(in: cancelBag)` - Store with auto-generated identifier, returns `AnyTask`
694+
- `task.store(in: cancelBag, withIdentifier: id)` - Store with a specific identifier, returns `AnyTask`
695+
696+
**AnyTask:**
697+
- `cancel()` - Cancel the underlying task
698+
- `waitComplete()` - Await completion of the underlying task
699+
- `isCancelled: Bool` - Whether the task has been cancelled
705700

706701
### AnyAsyncStream
707702

@@ -727,8 +722,9 @@ func observe<T>(stream: AnyAsyncStream<T>) async {
727722

728723
| Protocol | Purpose |
729724
|----------|---------|
730-
| `ScreenActionStore` | Actor-based protocol for ViewModels. Requires `binding(state:)` and `receive(action:)` |
725+
| `ScreenActionStore` | Actor-based protocol for ViewModels. Requires `receive(action:) async throws` and `viewState`. Provides `nonisolatedReceive` and centralized `dispatch` for loading/error handling |
731726
| `ActionLockable` | Provides a `lockKey` for action deduplication. Auto-conforms for `Hashable` types |
727+
| `NonPresentableError` | Protocol for errors that should be logged but not shown to the user (`isSilent: Bool`) |
732728
| `LoadingTrackable` | Declares whether an action should track loading state via `canTrackLoading` |
733729
| `StateUpdatable` | Provides `updateState(_:withAnimation:disablesAnimations:)` for batched state updates |
734730
| `PlaceholderRepresentable` | Declares `placeholder` and `isPlaceholder` for skeleton loading |
@@ -746,16 +742,17 @@ func observe<T>(stream: AnyAsyncStream<T>) async {
746742

747743
| Actor | Purpose |
748744
|-------|---------|
749-
| `ActionLocker` | Prevents duplicate action execution with `lock`, `unlock`, `canExecute`, `free` |
750-
| `CancelBag` | Task lifecycle management with identifier-based cancellation |
745+
| `ActionLocker` | Prevents duplicate action execution. Two variants: `.isolated` (actor) and `.nonIsolated` (class) |
746+
| `CancelBag` | Task lifecycle management with `DuplicatePolicy`, auto-removal of completed tasks, and identifier-based cancellation |
751747
| `StreamProducer<Element>` | Multi-subscriber async stream with optional latest-value replay |
752748

753749
### Structs
754750

755751
| Struct | Purpose |
756752
|--------|---------|
757-
| `AsyncAction<Input, Output>` | Generic async action wrapper with `execute` and `asyncExecute` |
758-
| `DisplayableError` | `LocalizedError` wrapper for displaying error alerts |
753+
| `AsyncAction<Input, Output>` | Generic async action wrapper with `execute`, `asyncExecute`, and `#isolation` support |
754+
| `DisplayableError` | `LocalizedError` wrapper with `originalError`, `isSilent`, and error routing support |
755+
| `AnyTask` | Public handle to a stored task with `cancel()`, `waitComplete()`, and `isCancelled` |
759756
| `AnyAsyncStream<Element>` | Type-erased `AsyncSequence` wrapper |
760757
| `RMLoadmoreView` | Pre-built `ProgressView` for load-more pagination |
761758

0 commit comments

Comments
 (0)