Skip to content
Merged
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
79 changes: 79 additions & 0 deletions docs/images/action-flow.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.3.6 Chrome/140.0.7339.249 Electron/38.8.0 Safari/537.36" version="29.3.6">
<diagram id="NmBio4kByxeUEJGxpuMX" name="Page-1">
<mxGraphModel dx="2946" dy="1672" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="827" pageHeight="3169" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2" parent="1" style="text;html=1;fontSize=18;fontStyle=1;align=center;verticalAlign=middle;fillColor=none;strokeColor=none;fontColor=#333333;" value="Action Flow — ActionLocker &amp; LoadingTrackable" vertex="1">
<mxGeometry height="36" width="600" x="79" y="-65" as="geometry" />
</mxCell>
<mxCell id="3" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=2;fontSize=12;fontStyle=1;" value="View dispatches&#xa;receive(action:)" vertex="1">
<mxGeometry height="60" width="150" x="40" y="80" as="geometry" />
</mxCell>
<mxCell id="4" edge="1" parent="1" source="3" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=1;exitY=0.5;entryX=0;entryY=0.5;" target="5" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="5" parent="1" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;fontSize=11;fontStyle=1;" value="ActionLocker&#xa;canExecute(action)?" vertex="1">
<mxGeometry height="90" width="160" x="230" y="65" as="geometry" />
</mxCell>
<mxCell id="6" edge="1" parent="1" source="5" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#b85450;exitX=0.5;exitY=0;entryX=0.5;entryY=1;" target="7" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="7" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#CCCCCC;strokeWidth=1;fontSize=10;fontStyle=2;fontColor=#999999;" value="false → return&#xa;(duplicate blocked)" vertex="1">
<mxGeometry height="50" width="116" x="252" y="-14" as="geometry" />
</mxCell>
<mxCell id="8" edge="1" parent="1" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#82b366;exitX=1;exitY=0.5;entryX=0;entryY=0.5;fontStyle=2;fontSize=18;fontColor=#82b366;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;labelBackgroundColor=none;" value="true">
<mxGeometry relative="1" x="0.05" y="-15" as="geometry">
<mxPoint as="offset" />
<mxPoint x="390" y="110" as="sourcePoint" />
<mxPoint x="430" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="9" parent="1" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;strokeWidth=2;fontSize=11;fontStyle=1;" value="canTrackLoading?" vertex="1">
<mxGeometry height="80" width="140" x="430" y="70" as="geometry" />
</mxCell>
<mxCell id="10" edge="1" parent="1" source="9" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#d6b656;exitX=1;exitY=0.5;entryX=0;entryY=0.5;fontStyle=2;fontSize=16;fontColor=#d6b656;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;labelBackgroundColor=none;" target="11" value="true">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="11" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;fontSize=11;fontStyle=1;" value="loadingStarted()&#xa;counter++" vertex="1">
<mxGeometry height="50" width="140" x="612" y="85" as="geometry" />
</mxCell>
<mxCell id="12" edge="1" parent="1" source="9" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#999999;dashed=1;exitX=0.5;exitY=1;entryX=0;entryY=0;fontStyle=2;fontSize=15;fontColor=#999999;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;labelBackgroundColor=none;" target="14" value="false">
<mxGeometry relative="1" x="-0.7086" y="-20" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="13" edge="1" parent="1" source="11" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0.5;exitY=1;entryX=1;entryY=0;" target="14" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="14" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=2;fontSize=12;fontStyle=1;" value="Execute action&#xa;(async do/catch)" vertex="1">
<mxGeometry height="53" width="160" x="517" y="204" as="geometry" />
</mxCell>
<mxCell id="15" edge="1" parent="1" source="14" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0;exitY=0.5;entryX=1;entryY=0.5;" target="16" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="16" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;fontSize=11;" value="Success:&#xa;update state" vertex="1">
<mxGeometry height="46" width="140" x="280" y="190" as="geometry" />
</mxCell>
<mxCell id="25" edge="1" parent="1" source="14" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#b85450;exitX=0;exitY=0.5;entryX=1;entryY=0.5;exitDx=0;exitDy=0;entryDx=0;entryDy=0;" target="17" value="">
<mxGeometry relative="1" as="geometry">
<mxPoint x="597" y="270" as="sourcePoint" />
<mxPoint x="420" y="296" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="17" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;fontSize=11;" value="Error:&#xa;showError()" vertex="1">
<mxGeometry height="46" width="140" x="280" y="260" as="geometry" />
</mxCell>
<mxCell id="18" edge="1" parent="1" source="16" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0;exitY=0.5;entryX=1;entryY=0.25;" target="20" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="19" edge="1" parent="1" source="17" style="endArrow=classic;html=1;strokeWidth=2;strokeColor=#666666;exitX=0;exitY=0.5;entryX=1;entryY=0.75;" target="20" value="">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="20" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;strokeWidth=2;fontSize=11;fontStyle=1;" value="unlock(action)&#xa;loadingFinished()&#xa;counter--" vertex="1">
<mxGeometry height="60" width="160" x="60" y="220" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
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 background="none">
<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