Skip to content
Closed
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
41 changes: 13 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,34 +63,9 @@ https://github.com/anthony1810/ScreenStateKit.git

ScreenStateKit promotes a clean architecture pattern for building features with three core components:

```
┌─────────────────────────────────────────────────────────────┐
│ SwiftUI View │
│ - Owns @State for ViewState and ViewModel │
│ - Binds state to ViewModel in .task modifier │
│ - Dispatches actions via viewModel.receive(action:) │
└─────────────────────────────────────────────────────────────┘
│ binds & dispatches
┌─────────────────────────────────────────────────────────────┐
│ ViewModel / Store (Actor) │
│ - Conforms to ScreenActionStore protocol │
│ - Holds weak reference to state │
│ - Processes actions with ActionLocker │
│ - Updates state on @MainActor │
└─────────────────────────────────────────────────────────────┘
│ updates
┌─────────────────────────────────────────────────────────────┐
│ ViewState (Observable) │
│ - Extends ScreenState │
│ - @Observable @MainActor class │
│ - Contains all UI state properties │
│ - Inherits loading/error handling │
└─────────────────────────────────────────────────────────────┘
```
<p align="center">
<img src="docs/images/architecture-overview.png" alt="Architecture Overview — The Three Pillars" width="700"/>
</p>

### The Three Pillars

Expand Down Expand Up @@ -206,6 +181,12 @@ actor FeatureViewStore: ScreenActionStore {
}
```

**Action Flow:** Here's how actions are processed through `ActionLocker` and `LoadingTrackable`:

<p align="center">
<img src="docs/images/action-flow.png" alt="Action Flow — ActionLocker &amp; LoadingTrackable" width="700"/>
</p>

### 3. Build the View

```swift
Expand Down Expand Up @@ -338,6 +319,10 @@ await viewState?.updateState({ state in

`ScreenState` supports parent-child relationships, where loading and error states propagate upward from a child state to a parent.

<p align="center">
<img src="docs/images/parent-child-binding.png" alt="Parent-Child State Binding" width="700"/>
</p>

```swift
public struct BindingParentStateOption: OptionSet, Sendable {
public static let loading // Propagate loading state
Expand Down
99 changes: 99 additions & 0 deletions docs/images/action-flow.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>

<!-- Title -->
<mxCell id="2" value="Action Flow — ActionLocker &amp; LoadingTrackable" style="text;html=1;fontSize=18;fontStyle=1;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="100" y="15" width="600" height="36" as="geometry"/>
</mxCell>

<!-- Step 1: View dispatches action -->
<mxCell id="3" value="View dispatches&#xa;receive(action:)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=2;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="40" y="80" width="150" height="60" as="geometry"/>
</mxCell>

<!-- Arrow 1→2 -->
<mxCell id="4" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=1;exitY=0.5;entryX=0;entryY=0.5;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>

<!-- Step 2: ActionLocker check -->
<mxCell id="5" value="ActionLocker&#xa;canExecute(action)?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;fontSize=11;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="230" y="65" width="160" height="90" as="geometry"/>
</mxCell>

<!-- Arrow: Already locked → return -->
<mxCell id="6" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#b85450;exitX=0.5;exitY=0;entryX=0.5;entryY=1;" edge="1" parent="1" source="5" target="7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="7" value="false → return&#xa;(duplicate blocked)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#CCCCCC;strokeWidth=1;fontSize=10;fontStyle=2;fontColor=#999999;" vertex="1" parent="1">
<mxGeometry x="252" y="2" width="116" height="50" as="geometry"/>
</mxCell>

<!-- Arrow: canExecute → LoadingTrackable check -->
<mxCell id="8" value="true" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#82b366;exitX=1;exitY=0.5;entryX=0;entryY=0.5;fontStyle=2;fontSize=10;fontColor=#82b366;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;" edge="1" parent="1" source="5" target="9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>

<!-- Step 3: LoadingTrackable check -->
<mxCell id="9" value="canTrackLoading?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;fontSize=11;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="430" y="70" width="140" height="80" as="geometry"/>
</mxCell>

<!-- Arrow: canTrackLoading true → loadingStarted -->
<mxCell id="10" value="true" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#d6b656;exitX=1;exitY=0.5;entryX=0;entryY=0.5;fontStyle=2;fontSize=10;fontColor=#d6b656;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;" edge="1" parent="1" source="9" target="11">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="loadingStarted()&#xa;counter++" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;fontSize=11;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="610" y="72" width="140" height="50" as="geometry"/>
</mxCell>

<!-- Arrow: canTrackLoading false → skip loading (goes to execute) -->
<mxCell id="12" value="false" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#999999;dashed=1;exitX=0.5;exitY=1;entryX=0;entryY=0;fontStyle=2;fontSize=10;fontColor=#999999;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;" edge="1" parent="1" source="9" target="14">
<mxGeometry relative="1" as="geometry"/>
</mxCell>

<!-- Arrow: loadingStarted → execute action -->
<mxCell id="13" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0.5;exitY=1;entryX=1;entryY=0;" edge="1" parent="1" source="11" target="14">
<mxGeometry relative="1" as="geometry"/>
</mxCell>

<!-- Step 4: Execute action -->
<mxCell id="14" value="Execute action&#xa;(async do/catch)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=2;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="480" y="180" width="160" height="60" as="geometry"/>
</mxCell>

<!-- Arrow: Execute → success/error split -->
<mxCell id="15" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0;exitY=0.5;entryX=1;entryY=0.5;" edge="1" parent="1" source="14" target="16">
<mxGeometry relative="1" as="geometry"/>
</mxCell>

<!-- Success path -->
<mxCell id="16" value="Success:&#xa;update state" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="280" y="190" width="140" height="46" as="geometry"/>
</mxCell>

<!-- Error path -->
<mxCell id="25" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#b85450;exitX=0.5;exitY=1;entryX=1;entryY=0.5;" edge="1" parent="1" source="14" target="17">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="17" value="Error:&#xa;showError()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="280" y="260" width="140" height="46" as="geometry"/>
</mxCell>

<!-- Arrow: both paths → cleanup -->
<mxCell id="18" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0;exitY=0.5;entryX=1;entryY=0.25;" edge="1" parent="1" source="16" target="20">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="19" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0;exitY=0.5;entryX=1;entryY=0.75;" edge="1" parent="1" source="17" target="20">
<mxGeometry relative="1" as="geometry"/>
</mxCell>

<!-- Step 5: Cleanup -->
<mxCell id="20" value="unlock(action)&#xa;loadingFinished()&#xa;counter--" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;strokeWidth=2;fontSize=11;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="60" y="220" width="160" height="60" as="geometry"/>
</mxCell>

</root>
</mxGraphModel>
Binary file added docs/images/action-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions docs/images/architecture-overview.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>

<!-- Title -->
<mxCell id="2" value="Architecture Overview — The Three Pillars" style="text;html=1;fontSize=18;fontStyle=1;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="100" y="20" width="600" height="40" as="geometry"/>
</mxCell>

<!-- SwiftUI View Box -->
<mxCell id="3" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=2;arcSize=12;" vertex="1" parent="1">
<mxGeometry x="150" y="80" width="500" height="120" as="geometry"/>
</mxCell>
<mxCell id="4" value="SwiftUI View" style="text;html=1;fontSize=16;fontStyle=1;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="310" y="82" width="180" height="30" as="geometry"/>
</mxCell>
<mxCell id="5" value="• Owns @State for ViewState and ViewModel&#xa;• Binds state to ViewModel in .task modifier&#xa;• Dispatches actions via viewModel.receive(action:)" style="text;html=1;fontSize=11;align=left;verticalAlign=top;fillColor=none;strokeColor=none;fontColor=#555555;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="180" y="112" width="440" height="70" as="geometry"/>
</mxCell>
<!-- View icon -->
<mxCell id="6" value="📱" style="text;html=1;fontSize=24;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="160" y="88" width="40" height="40" as="geometry"/>
</mxCell>

<!-- Arrow: View → ViewModel -->
<mxCell id="7" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#6c8ebf;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="3" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="8" value="binds &amp; dispatches" style="text;html=1;fontSize=11;fontStyle=2;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="420" y="210" width="120" height="20" as="geometry"/>
</mxCell>

<!-- ViewModel / Store Box -->
<mxCell id="10" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;arcSize=12;" vertex="1" parent="1">
<mxGeometry x="150" y="240" width="500" height="120" as="geometry"/>
</mxCell>
<mxCell id="11" value="ViewModel / Store (Actor)" style="text;html=1;fontSize=16;fontStyle=1;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="280" y="242" width="240" height="30" as="geometry"/>
</mxCell>
<mxCell id="12" value="• Conforms to ScreenActionStore protocol&#xa;• Processes actions with ActionLocker&#xa;• Updates state on @MainActor" style="text;html=1;fontSize=11;align=left;verticalAlign=top;fillColor=none;strokeColor=none;fontColor=#555555;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="180" y="272" width="440" height="70" as="geometry"/>
</mxCell>
<!-- ViewModel icon -->
<mxCell id="13" value="⚙️" style="text;html=1;fontSize=24;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="160" y="248" width="40" height="40" as="geometry"/>
</mxCell>

<!-- Arrow: ViewModel → ViewState -->
<mxCell id="14" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#82b366;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="10" target="17">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="15" value="updates" style="text;html=1;fontSize=11;fontStyle=2;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="420" y="370" width="80" height="20" as="geometry"/>
</mxCell>

<!-- ViewState Box -->
<mxCell id="17" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;arcSize=12;" vertex="1" parent="1">
<mxGeometry x="150" y="400" width="500" height="120" as="geometry"/>
</mxCell>
<mxCell id="18" value="ViewState (Observable)" style="text;html=1;fontSize=16;fontStyle=1;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="290" y="402" width="220" height="30" as="geometry"/>
</mxCell>
<mxCell id="19" value="• Extends ScreenState (@Observable @MainActor)&#xa;• Contains all UI state properties&#xa;• Inherits loading counter &amp; error handling" style="text;html=1;fontSize=11;align=left;verticalAlign=top;fillColor=none;strokeColor=none;fontColor=#555555;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="180" y="432" width="440" height="70" as="geometry"/>
</mxCell>
<!-- ViewState icon -->
<mxCell id="20" value="📊" style="text;html=1;fontSize=24;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="160" y="408" width="40" height="40" as="geometry"/>
</mxCell>

<!-- Feedback arrow: ViewState → View (left side) -->
<mxCell id="21" value="" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#d6b656;curved=1;exitX=0;exitY=0.5;entryX=0;entryY=0.5;" edge="1" parent="1" source="17" target="3">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="100" y="460"/>
<mxPoint x="100" y="140"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="22" value="@Observable&#xa;notifies" style="text;html=1;fontSize=11;fontStyle=2;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="30" y="280" width="80" height="40" as="geometry"/>
</mxCell>

</root>
</mxGraphModel>
Binary file added docs/images/architecture-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading