Skip to content

Commit 12d8d15

Browse files
committed
✨ Adds StateBinding.
1 parent e833e7f commit 12d8d15

File tree

4 files changed

+132
-1
lines changed

4 files changed

+132
-1
lines changed

.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(
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 = 2
34+
35+
let setterExpectation = expectation(description: "Expected binding setter.")
36+
37+
let count = LockIsolated(0)
38+
let binding = Binding(
39+
get: {
40+
getterExpectation.fulfill()
41+
return count.value
42+
},
43+
set: {
44+
setterExpectation.fulfill()
45+
count.setValue($0)
46+
}
47+
)
48+
@StateBinding var counter = 5
49+
_counter.externalBinding = binding
50+
binding.wrappedValue = 10
51+
XCTAssertEqual(counter, 10)
52+
await fulfillment(of: [setterExpectation, getterExpectation], enforceOrder: true)
53+
}
54+
}

0 commit comments

Comments
 (0)