Skip to content

Workaround for BindingAction existential layout crash #1881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 30, 2023
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
25 changes: 24 additions & 1 deletion Sources/ComposableArchitecture/SwiftUI/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,32 @@ public struct BindingAction<Root>: Equatable {

@usableFromInline
let set: (inout Root) -> Void
let value: Any
// NB: swift(<5.8) has an enum existential layout bug that can cause crashes when extracting
// payloads. We can box the existential to work around the bug.
#if swift(<5.8)
private let _value: [Any]
var value: Any { self._value[0] }
#else
let value: Any
#endif
let valueIsEqualTo: (Any) -> Bool

init(
keyPath: PartialKeyPath<Root>,
set: @escaping (inout Root) -> Void,
value: Any,
valueIsEqualTo: @escaping (Any) -> Bool
) {
self.keyPath = keyPath
self.set = set
#if swift(<5.8)
self._value = [value]
#else
self.value = value
#endif
self.valueIsEqualTo = valueIsEqualTo
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.keyPath == rhs.keyPath && lhs.valueIsEqualTo(rhs.value)
}
Expand Down
13 changes: 13 additions & 0 deletions Tests/ComposableArchitectureTests/BindingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,17 @@ final class BindingTests: XCTestCase {
XCTAssertEqual(viewStore.state, .init(nested: .init(field: "Hello!")))
}
#endif

// NB: This crashes in Swift(<5.8) RELEASE when `BindingAction` holds directly onto an unboxed
// `value: Any` existential
func testLayoutBug() {
enum Foo {
case bar(Baz)
}
enum Baz {
case fizz(BindingAction<Void>)
case buzz(Bool)
}
_ = (/Foo.bar).extract(from: .bar(.buzz(true)))
}
}
54 changes: 28 additions & 26 deletions Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,38 +458,40 @@
// Confirms that when you send an action the test store skips any unreceived actions
// automatically.
func testSendWithUnreceivedActions_SkipsActions() async {
struct Feature: ReducerProtocol {
enum Action: Equatable {
case tap
case response(Int)
}
func reduce(into state: inout Int, action: Action) -> EffectTask<Action> {
switch action {
case .tap:
state += 1
return .task { [state] in .response(state + 42) }
case let .response(number):
state = number
return .none
await _withMainSerialExecutor {
struct Feature: ReducerProtocol {
enum Action: Equatable {
case tap
case response(Int)
}
func reduce(into state: inout Int, action: Action) -> EffectTask<Action> {
switch action {
case .tap:
state += 1
return .task { [state] in .response(state + 42) }
case let .response(number):
state = number
return .none
}
}
}
}

let store = TestStore(
initialState: 0,
reducer: Feature()
)
store.exhaustivity = .off
let store = TestStore(
initialState: 0,
reducer: Feature()
)
store.exhaustivity = .off

await store.send(.tap)
XCTAssertEqual(store.state, 1)
await store.send(.tap)
XCTAssertEqual(store.state, 1)

// Ignored received action: .response(43)
await store.send(.tap)
XCTAssertEqual(store.state, 44)
// Ignored received action: .response(43)
await store.send(.tap)
XCTAssertEqual(store.state, 44)

await store.skipReceivedActions()
XCTAssertEqual(store.state, 86)
await store.skipReceivedActions()
XCTAssertEqual(store.state, 86)
}
}

func testPartialExhaustivityPrefix() async {
Expand Down