Skip to content

Commit c330496

Browse files
authored
Workaround for BindingAction existential layout crash (#1881)
* Add test case for binding action crash * Workaround layout issue * flakey test * wip
1 parent 98af2ad commit c330496

File tree

3 files changed

+65
-27
lines changed

3 files changed

+65
-27
lines changed

Sources/ComposableArchitecture/SwiftUI/Binding.swift

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

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

179+
init(
180+
keyPath: PartialKeyPath<Root>,
181+
set: @escaping (inout Root) -> Void,
182+
value: Any,
183+
valueIsEqualTo: @escaping (Any) -> Bool
184+
) {
185+
self.keyPath = keyPath
186+
self.set = set
187+
#if swift(<5.8)
188+
self._value = [value]
189+
#else
190+
self.value = value
191+
#endif
192+
self.valueIsEqualTo = valueIsEqualTo
193+
}
194+
172195
public static func == (lhs: Self, rhs: Self) -> Bool {
173196
lhs.keyPath == rhs.keyPath && lhs.valueIsEqualTo(rhs.value)
174197
}

Tests/ComposableArchitectureTests/BindingTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,17 @@ final class BindingTests: XCTestCase {
4141
XCTAssertEqual(viewStore.state, .init(nested: .init(field: "Hello!")))
4242
}
4343
#endif
44+
45+
// NB: This crashes in Swift(<5.8) RELEASE when `BindingAction` holds directly onto an unboxed
46+
// `value: Any` existential
47+
func testLayoutBug() {
48+
enum Foo {
49+
case bar(Baz)
50+
}
51+
enum Baz {
52+
case fizz(BindingAction<Void>)
53+
case buzz(Bool)
54+
}
55+
_ = (/Foo.bar).extract(from: .bar(.buzz(true)))
56+
}
4457
}

Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift

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

478-
let store = TestStore(
479-
initialState: 0,
480-
reducer: Feature()
481-
)
482-
store.exhaustivity = .off
479+
let store = TestStore(
480+
initialState: 0,
481+
reducer: Feature()
482+
)
483+
store.exhaustivity = .off
483484

484-
await store.send(.tap)
485-
XCTAssertEqual(store.state, 1)
485+
await store.send(.tap)
486+
XCTAssertEqual(store.state, 1)
486487

487-
// Ignored received action: .response(43)
488-
await store.send(.tap)
489-
XCTAssertEqual(store.state, 44)
488+
// Ignored received action: .response(43)
489+
await store.send(.tap)
490+
XCTAssertEqual(store.state, 44)
490491

491-
await store.skipReceivedActions()
492-
XCTAssertEqual(store.state, 86)
492+
await store.skipReceivedActions()
493+
XCTAssertEqual(store.state, 86)
494+
}
493495
}
494496

495497
func testPartialExhaustivityPrefix() async {

0 commit comments

Comments
 (0)