Skip to content

Commit 5f77d0a

Browse files
authored
Prevent case navigation binding from writing to other cases (pointfreeco#149)
* wip * wip * wip
1 parent 2ec6c3a commit 5f77d0a

File tree

2 files changed

+67
-8
lines changed

2 files changed

+67
-8
lines changed

Sources/SwiftUINavigation/Binding.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@
131131
fileprivate subscript<Member>(
132132
keyPath: KeyPath<Self.AllCasePaths, AnyCasePath<Self, Member>>
133133
) -> Member? {
134-
get { Self.allCasePaths[keyPath: keyPath].extract(from: self) }
134+
get {
135+
Self.allCasePaths[keyPath: keyPath].extract(from: self)
136+
}
135137
set {
136138
guard let newValue else { return }
137139
self = Self.allCasePaths[keyPath: keyPath].embed(newValue)
@@ -144,15 +146,13 @@
144146
keyPath: KeyPath<Wrapped.AllCasePaths, AnyCasePath<Wrapped, Member>>
145147
) -> Member? {
146148
get {
147-
guard let wrapped = self else { return nil }
148-
return Wrapped.allCasePaths[keyPath: keyPath].extract(from: wrapped)
149+
self.flatMap(Wrapped.allCasePaths[keyPath: keyPath].extract(from:))
149150
}
150151
set {
151-
guard let newValue else {
152-
self = nil
153-
return
154-
}
155-
self = Wrapped.allCasePaths[keyPath: keyPath].embed(newValue)
152+
let casePath = Wrapped.allCasePaths[keyPath: keyPath]
153+
guard self.flatMap(casePath.extract(from:)) != nil
154+
else { return }
155+
self = newValue.map(casePath.embed)
156156
}
157157
}
158158
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#if swift(>=5.9) && canImport(SwiftUI)
2+
import CustomDump
3+
import SwiftUI
4+
import SwiftUINavigation
5+
import XCTest
6+
7+
final class BindingTests: XCTestCase {
8+
@CasePathable
9+
@dynamicMemberLookup
10+
enum Status: Equatable {
11+
case inStock(quantity: Int)
12+
case outOfStock(isOnBackOrder: Bool)
13+
}
14+
15+
func testCaseLookup() throws {
16+
@Binding var status: Status
17+
_status = Binding(initialValue: .inStock(quantity: 1))
18+
19+
let inStock = try XCTUnwrap($status.inStock)
20+
inStock.wrappedValue += 1
21+
22+
XCTAssertEqual(status, .inStock(quantity: 2))
23+
}
24+
25+
func testCaseCannotReplaceOtherCase() throws {
26+
@Binding var status: Status
27+
_status = Binding(initialValue: .inStock(quantity: 1))
28+
29+
let inStock = try XCTUnwrap($status.inStock)
30+
31+
status = .outOfStock(isOnBackOrder: true)
32+
33+
inStock.wrappedValue = 42
34+
XCTAssertEqual(status, .outOfStock(isOnBackOrder: true))
35+
}
36+
37+
func testDestinationCannotReplaceOtherDestination() throws {
38+
@Binding var destination: Status?
39+
_destination = Binding(initialValue: .inStock(quantity: 1))
40+
41+
let inStock = try XCTUnwrap($destination.inStock)
42+
43+
destination = .outOfStock(isOnBackOrder: true)
44+
45+
inStock.wrappedValue = 42
46+
XCTAssertEqual(destination, .outOfStock(isOnBackOrder: true))
47+
}
48+
}
49+
50+
private extension Binding {
51+
init(initialValue: Value) {
52+
var value = initialValue
53+
self.init(
54+
get: { value },
55+
set: { value = $0 }
56+
)
57+
}
58+
}
59+
#endif // canImport(SwiftUI)

0 commit comments

Comments
 (0)