Skip to content

Commit 9f2f44f

Browse files
authored
Merge pull request #39 from connor-ricks/task/state-binding
✨ Adds StateBinding.
2 parents e833e7f + 85fe7d1 commit 9f2f44f

File tree

9 files changed

+212
-49
lines changed

9 files changed

+212
-49
lines changed

.github/workflows/checks.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: 🧪 Run tests
2626
run: xcodebuild test -scheme "swift-nibbles-Package" -testPlan "swift-nibbles-Package" -destination "OS=17.0,name=iPhone 15 Pro"
2727
- name: 📊 Upload Coverage
28-
uses: codecov/codecov-action@v3
28+
uses: codecov/codecov-action@v4
2929
with:
3030
token: ${{ secrets.CODECOV_TOKEN }}
3131
swift: true

.swiftpm/xcode/xcshareddata/xcschemes/HTTPNetworking.xcscheme renamed to .swiftpm/xcode/xcshareddata/xcschemes/StateBinding.xcscheme

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1500"
3+
LastUpgradeVersion = "1600"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"
7-
buildImplicitDependencies = "YES">
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
89
<BuildActionEntries>
910
<BuildActionEntry
1011
buildForTesting = "YES"
@@ -14,9 +15,9 @@
1415
buildForAnalyzing = "YES">
1516
<BuildableReference
1617
BuildableIdentifier = "primary"
17-
BlueprintIdentifier = "HTTPNetworking"
18-
BuildableName = "HTTPNetworking"
19-
BlueprintName = "HTTPNetworking"
18+
BlueprintIdentifier = "StateBinding"
19+
BuildableName = "StateBinding"
20+
BlueprintName = "StateBinding"
2021
ReferencedContainer = "container:">
2122
</BuildableReference>
2223
</BuildActionEntry>
@@ -26,13 +27,8 @@
2627
buildConfiguration = "Debug"
2728
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2829
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29-
shouldUseLaunchSchemeArgsEnv = "YES">
30-
<TestPlans>
31-
<TestPlanReference
32-
reference = "container:Tests/HTTPNetworkingTests/HTTPNetworking.xctestplan"
33-
default = "YES">
34-
</TestPlanReference>
35-
</TestPlans>
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
3632
</TestAction>
3733
<LaunchAction
3834
buildConfiguration = "Debug"
@@ -54,9 +50,9 @@
5450
<MacroExpansion>
5551
<BuildableReference
5652
BuildableIdentifier = "primary"
57-
BlueprintIdentifier = "HTTPNetworking"
58-
BuildableName = "HTTPNetworking"
59-
BlueprintName = "HTTPNetworking"
53+
BlueprintIdentifier = "StateBinding"
54+
BuildableName = "StateBinding"
55+
BlueprintName = "StateBinding"
6056
ReferencedContainer = "container:">
6157
</BuildableReference>
6258
</MacroExpansion>

.swiftpm/xcode/xcshareddata/xcschemes/swift-nibbles-Package.xcscheme

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@
9090
ReferencedContainer = "container:">
9191
</BuildableReference>
9292
</BuildActionEntry>
93+
<BuildActionEntry
94+
buildForTesting = "YES"
95+
buildForRunning = "YES"
96+
buildForProfiling = "YES"
97+
buildForArchiving = "YES"
98+
buildForAnalyzing = "YES">
99+
<BuildableReference
100+
BuildableIdentifier = "primary"
101+
BlueprintIdentifier = "StateBinding"
102+
BuildableName = "StateBinding"
103+
BlueprintName = "StateBinding"
104+
ReferencedContainer = "container:">
105+
</BuildableReference>
106+
</BuildActionEntry>
93107
</BuildActionEntries>
94108
</BuildAction>
95109
<TestAction
@@ -124,6 +138,16 @@
124138
ReferencedContainer = "container:">
125139
</BuildableReference>
126140
</TestableReference>
141+
<TestableReference
142+
skipped = "NO">
143+
<BuildableReference
144+
BuildableIdentifier = "primary"
145+
BlueprintIdentifier = "StateBindingTests"
146+
BuildableName = "StateBindingTests"
147+
BlueprintName = "StateBindingTests"
148+
ReferencedContainer = "container:">
149+
</BuildableReference>
150+
</TestableReference>
127151
</Testables>
128152
</TestAction>
129153
<LaunchAction

Package.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let package = Package(
1717
.library(name: "Identified", targets: ["Identified"]),
1818
.library(name: "SharedState", targets: ["SharedState"]),
1919
.library(name: "Stash", targets: ["Stash"]),
20+
.library(name: "StateBinding", targets: ["StateBinding"]),
2021
.plugin(name: "Create TCA Feature", targets: ["Create TCA Feature"])
2122
],
2223
dependencies: [
@@ -40,7 +41,10 @@ let package = Package(
4041

4142
.target(name: "Stash"),
4243
.testTarget(name: "StashTests", dependencies: ["Stash"]),
43-
44+
45+
.target(name: "StateBinding"),
46+
.testTarget(name: "StateBindingTests", dependencies: ["StateBinding", .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras")]),
47+
4448
.plugin(
4549
name: "Create TCA Feature",
4650
capability: .command(

README.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,9 @@ Nibbles are all broken down into their own targets, so you can choose which nibb
2424

2525
## Nibbles
2626

27-
### 🗄️ Stash
28-
A simple cache that can be used to store objects.
29-
30-
Use a `Stash` to store objects of a given type in memory using an associated key.
31-
You can then fetch attempt to retrieve from the `Stash` at a later time using the key.
32-
33-
### 📬 SharedState
34-
A simple container that encapsulates an object allowing others to subscribe to and monitor changes to the state.
35-
36-
Frequently use in PointFree's TCA architecture to subscribe long-running effects to shared state changes.
37-
38-
### ⛓️ Extensions
39-
A collection of useful extensions that I freqeuntly implement across multiple projects.
40-
41-
### ⚡️ Fuse
42-
A collection of useful Combine nibbles.
43-
44-
- A variety of helpful sinks that allow for easier less verbose interactions with Combine publishers.
45-
- A variety of helpful sinks that automatically cleanup after themselves by using a `DisposableBag`.
46-
- `BuffableAsyncPublisher` and `BuffableAsyncThrowingPublisher` which both expose a `values(bufferingStrategy:)` on `Publisher`
47-
- This is a more configurable and powerful version of `values` in Combine that allows converting Combine to an async/await syntax.
48-
4927
### 🛜 Exchange
5028

5129
#### HTTPClient
52-
5330
An HTTP client that creates and manages requests over the network.
5431

5532
The client provides support for sharing common functionality across all requests, but each request can also layer on additional functionality if needed.
@@ -65,15 +42,38 @@ have their own nuance and complexities, and encapsulating all of that in one pla
6542
more scalable and testable way.
6643

6744
#### Socket
68-
6945
A websocket created from a URL that can listen to messages send through the connection using `AsyncStream`.
7046

7147
Sending messages and cancelling the connection is as easy as calling a few methods.
7248

73-
### 🏷️ Identified
49+
### ⛓️ Extensions
50+
A collection of useful extensions that I freqeuntly implement across multiple projects.
51+
52+
### ⚡️ Fuse
53+
A collection of useful Combine nibbles.
7454

55+
- A variety of helpful sinks that allow for easier less verbose interactions with Combine publishers.
56+
- A variety of helpful sinks that automatically cleanup after themselves by using a `DisposableBag`.
57+
- `BuffableAsyncPublisher` and `BuffableAsyncThrowingPublisher` which both expose a `values(bufferingStrategy:)` on `Publisher`
58+
- This is a more configurable and powerful version of `values` in Combine that allows converting Combine to an async/await syntax.
59+
60+
### 🏷️ Identified
7561
A protocol for marking objects as identified and allowing interaction with their identifiers in a type-safe way.
7662

63+
### 📬 SharedState
64+
A simple container that encapsulates an object allowing others to subscribe to and monitor changes to the state.
65+
66+
Frequently use in PointFree's TCA architecture to subscribe long-running effects to shared state changes.
67+
68+
### 🗄️ Stash
69+
A simple cache that can be used to store objects.
70+
71+
Use a `Stash` to store objects of a given type in memory using an associated key.
72+
You can then fetch attempt to retrieve from the `Stash` at a later time using the key.
73+
74+
### 🔗 StateBinding
75+
A simple property wrapper that allows your views to optionally take a binding and default to internal state. Useful when creating reusable components.
76+
7777
## Contributing
7878

7979
[Learn more](https://github.com/connor-ricks/swift-nibbles/blob/main/CONTRIBUTING.md)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2024 Connor Ricks
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
24+
import SwiftUI
25+
26+
@propertyWrapper
27+
public struct StateBinding<Value>: DynamicProperty {
28+
29+
// MARK: Properties
30+
31+
@State private var state: Value
32+
33+
public var externalBinding: Binding<Value>? = nil
34+
35+
public var wrappedValue: Value {
36+
get { projectedValue.wrappedValue }
37+
nonmutating set { projectedValue.wrappedValue = newValue }
38+
}
39+
40+
public var projectedValue: Binding<Value> {
41+
externalBinding ?? $state
42+
}
43+
44+
// MARK: Initializers
45+
46+
public init(wrappedValue: Value) {
47+
_state = .init(initialValue: wrappedValue)
48+
}
49+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configurations" : [
3+
{
4+
"id" : "F058B37F-5474-46C0-A432-D042453493AB",
5+
"name" : "Configuration 1",
6+
"options" : {
7+
8+
}
9+
}
10+
],
11+
"defaultOptions" : {
12+
13+
},
14+
"testTargets" : [
15+
{
16+
"target" : {
17+
"containerPath" : "container:",
18+
"identifier" : "StateBindingTests",
19+
"name" : "StateBindingTests"
20+
}
21+
}
22+
],
23+
"version" : 1
24+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2024 Connor Ricks
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
24+
@testable import StateBinding
25+
import ConcurrencyExtras
26+
import SwiftUI
27+
import XCTest
28+
29+
@MainActor
30+
class StateBindingTests: XCTestCase {
31+
func test_stateBinding_whenProvidedExternalBinding_doesUseExternalBinding() async {
32+
let getterExpectation = expectation(description: "Expected binding getter.")
33+
getterExpectation.expectedFulfillmentCount = 3
34+
35+
let setterExpectation = expectation(description: "Expected binding setter.")
36+
setterExpectation.expectedFulfillmentCount = 2
37+
38+
let count = LockIsolated(0)
39+
let binding = Binding(
40+
get: {
41+
getterExpectation.fulfill()
42+
return count.value
43+
},
44+
set: {
45+
setterExpectation.fulfill()
46+
count.setValue($0)
47+
}
48+
)
49+
@StateBinding var counter = 5
50+
_counter.externalBinding = binding
51+
binding.wrappedValue = 10
52+
XCTAssertEqual(counter, 10)
53+
54+
counter = 15
55+
XCTAssertEqual(counter, 15)
56+
57+
await fulfillment(of: [setterExpectation, getterExpectation], enforceOrder: false)
58+
}
59+
}

Tests/swift-nibbles-Package.xctestplan

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
"testTimeoutsEnabled" : true
1515
},
1616
"testTargets" : [
17+
{
18+
"parallelizable" : true,
19+
"target" : {
20+
"containerPath" : "container:",
21+
"identifier" : "ExchangeTests",
22+
"name" : "ExchangeTests"
23+
}
24+
},
1725
{
1826
"parallelizable" : true,
1927
"target" : {
@@ -42,24 +50,23 @@
4250
"parallelizable" : true,
4351
"target" : {
4452
"containerPath" : "container:",
45-
"identifier" : "StashTests",
46-
"name" : "StashTests"
53+
"identifier" : "SharedStateTests",
54+
"name" : "SharedStateTests"
4755
}
4856
},
4957
{
5058
"parallelizable" : true,
5159
"target" : {
5260
"containerPath" : "container:",
53-
"identifier" : "ExchangeTests",
54-
"name" : "ExchangeTests"
61+
"identifier" : "StashTests",
62+
"name" : "StashTests"
5563
}
5664
},
5765
{
58-
"parallelizable" : true,
5966
"target" : {
6067
"containerPath" : "container:",
61-
"identifier" : "SharedStateTests",
62-
"name" : "SharedStateTests"
68+
"identifier" : "StateBindingTests",
69+
"name" : "StateBindingTests"
6370
}
6471
}
6572
],

0 commit comments

Comments
 (0)