Skip to content

Commit d9e72f3

Browse files
authored
Use better binding transformations where possible (pointfreeco#141)
* Use better binding transformations where possible `Binding.init(get:set)` is handy but buggy when it comes to preserving animations and passing in the correct transaction. We can preserve the binding's transaction in a less buggy manner by leveraging dynamic member lookup instead, a trick we've employed in TCA, but never here. * wip
1 parent a7592b6 commit d9e72f3

File tree

3 files changed

+72
-36
lines changed

3 files changed

+72
-36
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let package = Package(
2222
],
2323
dependencies: [
2424
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
25-
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"),
25+
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.2.2"),
2626
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
2727
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"),
2828
],

Sources/SwiftUINavigation/Binding.swift

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,7 @@
1414
dynamicMember keyPath: KeyPath<Value.AllCasePaths, AnyCasePath<Value, Member>>
1515
) -> Binding<Member>?
1616
where Value: CasePathable {
17-
let casePath = Value.allCasePaths[keyPath: keyPath]
18-
return Binding<Member>(
19-
unwrapping: Binding<Member?>(
20-
get: { casePath.extract(from: self.wrappedValue) },
21-
set: { newValue, transaction in
22-
guard let newValue else { return }
23-
self.transaction(transaction).wrappedValue = casePath.embed(newValue)
24-
}
25-
)
26-
)
17+
Binding<Member>(unwrapping: self[keyPath])
2718
}
2819

2920
/// Returns a binding to the associated value of a given case key path.
@@ -36,20 +27,7 @@
3627
dynamicMember keyPath: KeyPath<Enum.AllCasePaths, AnyCasePath<Enum, Member>>
3728
) -> Binding<Member?>
3829
where Value == Enum? {
39-
let casePath = Enum.allCasePaths[keyPath: keyPath]
40-
return Binding<Member?>(
41-
get: {
42-
guard let wrappedValue = self.wrappedValue else { return nil }
43-
return casePath.extract(from: wrappedValue)
44-
},
45-
set: { newValue, transaction in
46-
guard let newValue else {
47-
self.transaction(transaction).wrappedValue = nil
48-
return
49-
}
50-
self.transaction(transaction).wrappedValue = casePath.embed(newValue)
51-
}
52-
)
30+
self[keyPath]
5331
}
5432
#endif
5533

@@ -68,7 +46,8 @@
6846
/// - Parameter base: A value to project to an unwrapped value.
6947
/// - Returns: A new binding or `nil` when `base` is `nil`.
7048
public init?(unwrapping base: Binding<Value?>) {
71-
self.init(unwrapping: base, case: AnyCasePath(\.some))
49+
guard let value = base.wrappedValue else { return nil }
50+
self = base[default: DefaultSubscript(value)]
7251
}
7352

7453
/// Creates a binding by projecting the current optional value to a boolean describing if it's
@@ -79,14 +58,7 @@
7958
/// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`.
8059
public func isPresent<Wrapped>() -> Binding<Bool>
8160
where Value == Wrapped? {
82-
.init(
83-
get: { self.wrappedValue != nil },
84-
set: { isPresent, transaction in
85-
if !isPresent {
86-
self.transaction(transaction).wrappedValue = nil
87-
}
88-
}
89-
)
61+
self._isPresent
9062
}
9163

9264
/// Creates a binding that ignores writes to its wrapped value when equivalent to the new value.
@@ -139,4 +111,68 @@
139111
)
140112
}
141113
}
114+
115+
extension Optional {
116+
fileprivate var _isPresent: Bool {
117+
get { self != nil }
118+
set {
119+
guard !newValue else { return }
120+
self = nil
121+
}
122+
}
123+
124+
fileprivate subscript(default defaultSubscript: DefaultSubscript<Wrapped>) -> Wrapped {
125+
get {
126+
defaultSubscript.value = self ?? defaultSubscript.value
127+
return defaultSubscript.value
128+
}
129+
set {
130+
defaultSubscript.value = newValue
131+
if self != nil { self = newValue }
132+
}
133+
}
134+
}
135+
136+
private final class DefaultSubscript<Value>: Hashable {
137+
var value: Value
138+
init(_ value: Value) {
139+
self.value = value
140+
}
141+
static func == (lhs: DefaultSubscript, rhs: DefaultSubscript) -> Bool {
142+
lhs === rhs
143+
}
144+
func hash(into hasher: inout Hasher) {
145+
hasher.combine(ObjectIdentifier(self))
146+
}
147+
}
148+
149+
extension CasePathable {
150+
fileprivate subscript<Member>(
151+
keyPath: KeyPath<Self.AllCasePaths, AnyCasePath<Self, Member>>
152+
) -> Member? {
153+
get { Self.allCasePaths[keyPath: keyPath].extract(from: self) }
154+
set {
155+
guard let newValue else { return }
156+
self = Self.allCasePaths[keyPath: keyPath].embed(newValue)
157+
}
158+
}
159+
}
160+
161+
extension Optional where Wrapped: CasePathable {
162+
fileprivate subscript<Member>(
163+
keyPath: KeyPath<Wrapped.AllCasePaths, AnyCasePath<Wrapped, Member>>
164+
) -> Member? {
165+
get {
166+
guard let wrapped = self else { return nil }
167+
return Wrapped.allCasePaths[keyPath: keyPath].extract(from: wrapped)
168+
}
169+
set {
170+
guard let newValue else {
171+
self = nil
172+
return
173+
}
174+
self = Wrapped.allCasePaths[keyPath: keyPath].embed(newValue)
175+
}
176+
}
177+
}
142178
#endif // canImport(SwiftUI)

SwiftUINavigation.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)