Skip to content

Commit 116496f

Browse files
Promote Void? bindings to Bool via dynamic member lookup (#274)
* Promote `Void?` bindings to `Bool` via dynamic member lookup We have an explicit initializer for this conversion, but it's not super discoverable and it's generalized to all optionals. We can make a shorthand for `Void` payloads, which simplifies presentation: ```diff -.sheet(isPresented: Binding($destination.sheet)) { +.sheet(isPresented: $destination.sheet) { // ... } ``` Other changes: - `SwiftUI.Binding<Bool>(Binding<Optional>)`'s implementation strayed slightly from UIKit's `UIBinding` implementation, where issues were not reported. This is fixed. - Added missing documentation. * Add test for writing true to a UIBinding derived from optional. --------- Co-authored-by: Brandon Williams <[email protected]>
1 parent 238c19f commit 116496f

File tree

6 files changed

+82
-6
lines changed

6 files changed

+82
-6
lines changed

Sources/SwiftNavigation/Binding.swift

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
11
#if canImport(SwiftUI)
2+
import IssueReporting
23
import SwiftUI
34

45
extension Binding {
56
/// Creates a binding by projecting the base optional value to a Boolean value.
67
///
7-
/// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing.
8+
/// Writing `false` to the binding will `nil` out the base value. Writing `true` produces a
9+
/// runtime warning.
810
///
911
/// - Parameter base: A value to project to a Boolean value.
10-
public init<V>(_ base: Binding<V?>) where Value == Bool {
11-
self = base._isPresent
12+
public init<V>(
13+
_ base: Binding<V?>,
14+
fileID: StaticString = #fileID,
15+
filePath: StaticString = #filePath,
16+
line: UInt = #line,
17+
column: UInt = #column
18+
) where Value == Bool {
19+
self = base[
20+
fileID: HashableStaticString(rawValue: fileID),
21+
filePath: HashableStaticString(rawValue: filePath),
22+
line: line,
23+
column: column
24+
]
1225
}
1326
}
1427

1528
extension Optional {
16-
fileprivate var _isPresent: Bool {
29+
fileprivate subscript(
30+
fileID fileID: HashableStaticString,
31+
filePath filePath: HashableStaticString,
32+
line line: UInt,
33+
column column: UInt
34+
) -> Bool {
1735
get { self != nil }
1836
set {
19-
guard !newValue else { return }
20-
self = nil
37+
if newValue {
38+
reportIssue(
39+
"""
40+
Boolean presentation binding attempted to write 'true' to a generic 'Binding<Item?>' \
41+
(i.e., 'Binding<\(Wrapped.self)?>').
42+
43+
This is not a valid thing to do, as there is no way to convert 'true' to a new \
44+
instance of '\(Wrapped.self)'.
45+
""",
46+
fileID: fileID.rawValue,
47+
filePath: filePath.rawValue,
48+
line: line,
49+
column: column
50+
)
51+
} else {
52+
self = nil
53+
}
2154
}
2255
}
2356
}

Sources/SwiftNavigation/Documentation.docc/Extensions/UIBinding.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- ``init(_:)-1p53m``
99
- ``init(_:)-3ww0m``
1010
- ``init(_:)-9wed9``
11+
- ``init(_:fileID:filePath:line:column:)``
1112
- ``init(projectedValue:)``
1213
- ``constant(_:)``
1314

@@ -17,7 +18,9 @@
1718
- ``projectedValue``
1819
- ``subscript(dynamicMember:)-61aos``
1920
- ``subscript(dynamicMember:)-95b8x``
21+
- ``subscript(dynamicMember:)-xef5``
2022
- ``subscript(dynamicMember:)-lmkz``
23+
- ``subscript(dynamicMember:)-63hti``
2124

2225
### Managing changes
2326

Sources/SwiftNavigation/UIBinding.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,17 @@ public struct UIBinding<Value>: Sendable {
441441
return open(location)
442442
}
443443

444+
/// Returns a Boolean binding to a case of a given case key path with no associated value.
445+
///
446+
/// - Parameter keyPath: A case key path to a case with no associated value.
447+
/// - Returns: A new binding.
448+
public subscript<V: CasePathable>(
449+
dynamicMember keyPath: KeyPath<V.AllCasePaths, AnyCasePath<V, Void>>
450+
) -> UIBinding<Bool>
451+
where Value == V? {
452+
UIBinding<Bool>(self[dynamicMember: keyPath])
453+
}
454+
444455
/// Specifies a transaction for the binding.
445456
///
446457
/// - Parameter transaction: An instance of a ``UITransaction``.

Sources/SwiftUINavigation/Binding.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@
2929
self[keyPath]
3030
}
3131

32+
/// Returns a binding to a Boolean for a given case key path to a case without an associated
33+
/// value.
34+
///
35+
/// Useful for driving navigation off an optional enumeration of destinations for navigation
36+
/// APIs that take a Boolean binding.
37+
///
38+
/// - Parameter keyPath: A case key path to a specific associated value.
39+
/// - Returns: A new binding.
40+
public subscript<Enum: CasePathable>(
41+
dynamicMember keyPath: KeyPath<Enum.AllCasePaths, AnyCasePath<Enum, Void>>
42+
) -> Binding<Bool>
43+
where Value == Enum? {
44+
Binding<Bool>(self[keyPath])
45+
}
46+
3247
/// Creates a binding by projecting the base value to an unwrapped value.
3348
///
3449
/// Useful for producing non-optional bindings from optional ones.

Sources/SwiftUINavigation/Documentation.docc/Articles/Bindings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct SignInView: View {
7070
### Dynamic case lookup
7171

7272
- ``SwiftUI/Binding/subscript(dynamicMember:)-9abgy``
73+
- ``SwiftUI/Binding/subscript(dynamicMember:)-4i40p``
7374
- ``SwiftUI/Binding/subscript(dynamicMember:)-8vc80``
7475

7576
### Unwrapping bindings

Tests/SwiftNavigationTests/UIBindingTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ final class UIBindingTests: XCTestCase {
4444
count = 1729
4545
XCTAssertEqual(count, 1729)
4646
XCTAssertEqual(unwrappedCountBinding.wrappedValue, 1729)
47+
48+
XCTExpectFailure {
49+
let isCountPresent: UIBinding<Bool> = UIBinding($count)
50+
isCountPresent.wrappedValue = true
51+
} issueMatcher: {
52+
$0.compactDescription == """
53+
failed - Boolean presentation binding attempted to write 'true' to a generic 'UIBinding<Item?>' \
54+
(i.e., 'UIBinding<Int?>').
55+
56+
This is not a valid thing to do, as there is no way to convert 'true' to a new instance of \
57+
'Int'.
58+
"""
59+
}
4760
}
4861

4962
func testOperationToOptional() {

0 commit comments

Comments
 (0)