Skip to content

Commit 7f62abd

Browse files
stephencelisp4checo
authored andcommitted
Workaround for BindingAction existential layout crash (#1881)
* Add test case for binding action crash * Workaround layout issue * flakey test * wip (cherry picked from commit c3304961646bd6b6fd71e311d48950b1d63bd930) # Conflicts: # Tests/ComposableArchitectureTests/BindingTests.swift
1 parent c1acd01 commit 7f62abd

File tree

3 files changed

+98
-60
lines changed

3 files changed

+98
-60
lines changed

Sources/ComposableArchitecture/SwiftUI/Binding.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,32 @@ public struct BindingAction<Root>: Equatable {
171171

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

184+
init(
185+
keyPath: PartialKeyPath<Root>,
186+
set: @escaping (inout Root) -> Void,
187+
value: Any,
188+
valueIsEqualTo: @escaping (Any) -> Bool
189+
) {
190+
self.keyPath = keyPath
191+
self.set = set
192+
#if swift(<5.8)
193+
self._value = [value]
194+
#else
195+
self.value = value
196+
#endif
197+
self.valueIsEqualTo = valueIsEqualTo
198+
}
199+
177200
public static func == (lhs: Self, rhs: Self) -> Bool {
178201
lhs.keyPath == rhs.keyPath && lhs.valueIsEqualTo(rhs.value)
179202
}
Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,59 @@
11
#if canImport(SwiftUI)
2-
import ComposableArchitecture
3-
import XCTest
4-
5-
@MainActor
6-
final class BindingTests: XCTestCase {
7-
#if swift(>=5.7)
8-
func testNestedBindingState() {
9-
struct BindingTest: ReducerProtocol {
10-
struct State: Equatable {
11-
@BindingState var nested = Nested()
12-
13-
struct Nested: Equatable {
14-
var field = ""
15-
}
2+
import ComposableArchitecture
3+
import XCTest
4+
5+
@MainActor
6+
final class BindingTests: XCTestCase {
7+
#if swift(>=5.7)
8+
func testNestedBindingState() {
9+
struct BindingTest: ReducerProtocol {
10+
struct State: Equatable {
11+
@BindingState var nested = Nested()
12+
13+
struct Nested: Equatable {
14+
var field = ""
1615
}
16+
}
1717

18-
enum Action: BindableAction, Equatable {
19-
case binding(BindingAction<State>)
20-
}
18+
enum Action: BindableAction, Equatable {
19+
case binding(BindingAction<State>)
20+
}
2121

22-
var body: some ReducerProtocol<State, Action> {
23-
BindingReducer()
24-
Reduce { state, action in
25-
switch action {
26-
case .binding(\.$nested.field):
27-
state.nested.field += "!"
28-
return .none
29-
default:
30-
return .none
31-
}
22+
var body: some ReducerProtocol<State, Action> {
23+
BindingReducer()
24+
Reduce { state, action in
25+
switch action {
26+
case .binding(\.$nested.field):
27+
state.nested.field += "!"
28+
return .none
29+
default:
30+
return .none
3231
}
3332
}
3433
}
34+
}
3535

36-
let store = Store(initialState: BindingTest.State(), reducer: BindingTest())
36+
let store = Store(initialState: BindingTest.State(), reducer: BindingTest())
3737

38-
let viewStore = ViewStore(store, observe: { $0 })
38+
let viewStore = ViewStore(store, observe: { $0 })
3939

40-
viewStore.binding(\.$nested.field).wrappedValue = "Hello"
40+
viewStore.binding(\.$nested.field).wrappedValue = "Hello"
4141

42-
XCTAssertEqual(viewStore.state, .init(nested: .init(field: "Hello!")))
43-
}
44-
#endif
42+
XCTAssertEqual(viewStore.state, .init(nested: .init(field: "Hello!")))
43+
}
44+
#endif
45+
46+
// NB: This crashes in Swift(<5.8) RELEASE when `BindingAction` holds directly onto an unboxed
47+
// `value: Any` existential
48+
func testLayoutBug() {
49+
enum Foo {
50+
case bar(Baz)
51+
}
52+
enum Baz {
53+
case fizz(BindingAction<Void>)
54+
case buzz(Bool)
55+
}
56+
_ = (/Foo.bar).extract(from: .bar(.buzz(true)))
4557
}
58+
}
4659
#endif

Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -460,38 +460,40 @@
460460
// Confirms that when you send an action the test store skips any unreceived actions
461461
// automatically.
462462
func testSendWithUnreceivedActions_SkipsActions() async {
463-
struct Feature: ReducerProtocol {
464-
enum Action: Equatable {
465-
case tap
466-
case response(Int)
467-
}
468-
func reduce(into state: inout Int, action: Action) -> EffectTask<Action> {
469-
switch action {
470-
case .tap:
471-
state += 1
472-
return .task { [state] in .response(state + 42) }
473-
case let .response(number):
474-
state = number
475-
return .none
463+
await _withMainSerialExecutor {
464+
struct Feature: ReducerProtocol {
465+
enum Action: Equatable {
466+
case tap
467+
case response(Int)
468+
}
469+
func reduce(into state: inout Int, action: Action) -> EffectTask<Action> {
470+
switch action {
471+
case .tap:
472+
state += 1
473+
return .task { [state] in .response(state + 42) }
474+
case let .response(number):
475+
state = number
476+
return .none
477+
}
476478
}
477479
}
478-
}
479480

480-
let store = TestStore(
481-
initialState: 0,
482-
reducer: Feature()
483-
)
484-
store.exhaustivity = .off
481+
let store = TestStore(
482+
initialState: 0,
483+
reducer: Feature()
484+
)
485+
store.exhaustivity = .off
485486

486-
await store.send(.tap)
487-
XCTAssertEqual(store.state, 1)
487+
await store.send(.tap)
488+
XCTAssertEqual(store.state, 1)
488489

489-
// Ignored received action: .response(43)
490-
await store.send(.tap)
491-
XCTAssertEqual(store.state, 44)
490+
// Ignored received action: .response(43)
491+
await store.send(.tap)
492+
XCTAssertEqual(store.state, 44)
492493

493-
await store.skipReceivedActions()
494-
XCTAssertEqual(store.state, 86)
494+
await store.skipReceivedActions()
495+
XCTAssertEqual(store.state, 86)
496+
}
495497
}
496498

497499
func testPartialExhaustivityPrefix() async {

0 commit comments

Comments
 (0)