Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Sources/SwiftNavigation/CaseBindable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,38 @@
}
}

#if Perception
@available(iOS, introduced: 13, obsoleted: 17)
@available(macOS, introduced: 10.15, obsoleted: 14)
@available(tvOS, introduced: 13, obsoleted: 17)
@available(watchOS, introduced: 6, obsoleted: 10)
@available(visionOS, unavailable)
extension Perception.Bindable {
/// Derives an enumeration of bindings that can be switched over exhaustively _via_ dynamic
/// member lookup.
///
/// You don't call this subscript directly. Instead, Swift calls it for you when you access a
/// property from the underlying `Value` directly on this bindable object:
///
/// ```swift
/// @Perception.Bindable var item: Item
/// // ...
/// switch $item.status {
/// case .inStock(let $quantity):
/// Stepper("Quantity: \($quantity.wrappedValue)", value: $quantity)
/// case .outOfStock(let $isOnBackOrder):
/// Toggle("Is on back order?", isOn: $isOnBackOrder)
/// }
/// ```
public subscript<Member: CaseBindable>(
dynamicMember keyPath: ReferenceWritableKeyPath<Value, Member>
) -> Member.BindingEnumeration where Value: AnyObject {
let binding: SwiftUI.Binding<Member> = self[dynamicMember: keyPath]
return binding.cases
}
}
#endif

extension SwiftUI.Binding {
public var _$wrappedValue: Value { wrappedValue }
}
Expand Down
72 changes: 72 additions & 0 deletions Tests/SwiftNavigationTests/CaseBindableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import SwiftUI
#endif

#if Perception
import Perception
#endif

final class CaseBindableTests: XCTestCase {
func testCasePathableConformance() {
let status = Status.inStock(quantity: 100)
Expand Down Expand Up @@ -103,6 +107,74 @@
#endif
}

#if canImport(SwiftUI) && Perception
@available(iOS, introduced: 16, deprecated: 17, obsoleted: 17)
@available(macOS, introduced: 13, deprecated: 14, obsoleted: 14)
@available(tvOS, introduced: 16, deprecated: 17, obsoleted: 17)
@available(watchOS, introduced: 9, deprecated: 10, obsoleted: 10)
final class PerceptionBindableCaseBindableTests: XCTestCase {
override func setUp() async throws {
guard !deploymentTargetIncludesObservation() else {
throw XCTSkip(
"""
PerceptionTests were built against a deployment target too recent for perception checking.

To force these tests to run on macOS, you can override the target OS version explicitly as:

swift test -Xswiftc -target -Xswiftc arm64-apple-macosx13.0
"""
)
}
}

@MainActor
func testPerceptionBindableEnumerationDerivesBinding() {
@Perception.Bindable var ref = PerceptionRef(item: Item(name: "name", status: .inStock(quantity: 100)))
switch $ref.status.cases {
case .inStock(let quantity):
XCTAssertEqual(quantity.wrappedValue, 100)
quantity.wrappedValue = 7
case .outOfStock, .discontinued, .onSale:
XCTFail("Expected 'inStock'")
}
XCTAssertEqual(ref.status, .inStock(quantity: 7))
}

@MainActor
func testPerceptionBindableChainsIntoCaseBindableMember() {
@Perception.Bindable var ref = PerceptionRef(item: Item(name: "name", status: .inStock(quantity: 100)))
switch $ref.item.status.cases {
case .inStock(let quantity):
quantity.wrappedValue = 3
case .outOfStock, .onSale, .discontinued:
XCTFail("Expected 'inStock'")
}
XCTAssertEqual(ref.item.status, .inStock(quantity: 3))
}
}
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
private func deploymentTargetIncludesObservation() -> Bool { true }

@_disfavoredOverload
private func deploymentTargetIncludesObservation(_: Void = ()) -> Bool { false }

@Perceptible
private final class PerceptionRef {
var status: Status {
get {
item.status
}
set {
item.status = newValue
}
}
var item: Item
init(item: Item) {
self.item = item
}
}
#endif

private struct Item {
var name: String
var status: Status
Expand Down