Skip to content

Commit 37385fb

Browse files
authored
fix(gesture): prevent layout gesture reset crashes (#1085)
1 parent 4b84bd3 commit 37385fb

2 files changed

Lines changed: 114 additions & 9 deletions

File tree

Sources/OpenSwiftUICore/Event/Gesture/LayoutGesture.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,16 @@ private final class LayoutGestureBox {
171171
debugData = .reset(attribute.value)
172172
}
173173
phase = nil
174-
subgraph?.willInvalidate(isInserted: true)
175-
subgraph?.invalidate()
174+
if let subgraph {
175+
subgraph.willInvalidate(isInserted: true)
176+
subgraph.invalidate()
177+
}
176178
subgraph = nil
177179
responder.resetGesture()
178180
}
179181
events = [:]
180182
seenEventIDs = []
181-
resetDelta &+= 1
183+
resetDelta.unsafeIncrement()
182184
}
183185
}
184186

@@ -194,8 +196,7 @@ private final class LayoutGestureBox {
194196
}
195197
self.resetSeed = resetSeed
196198
for index in children.indices {
197-
children[index].reset()
198-
seed &+= 1
199+
resetChild(at: index)
199200
}
200201
seed &+= 1
201202
}
@@ -223,8 +224,7 @@ private final class LayoutGestureBox {
223224
}
224225
while index < count {
225226
count -= 1
226-
children[count].reset()
227-
seed &+= 1
227+
resetChild(at: count)
228228
children.removeLast()
229229
changed = true
230230
}
@@ -292,10 +292,22 @@ private final class LayoutGestureBox {
292292
children[index].phase!.value.isTerminal else {
293293
continue
294294
}
295-
children[index].reset()
296-
seed &+= 1
295+
resetChild(at: index)
297296
}
298297
}
298+
299+
private func resetChild(at index: Int) {
300+
// OpenSwiftUI Addition:
301+
// SwiftUI's original implementation will trigger a crash with
302+
// SWIFTUI_GESTURE_CONTAINER=0 && SWIFTUI_EVENT_DEBUG=G
303+
//
304+
// children[index].reset()
305+
var child = children[index]
306+
child.reset()
307+
children[index] = child
308+
309+
seed.unsafeIncrement()
310+
}
299311
}
300312

301313
// MARK: - LayoutGestureChildProxy

Tests/OpenSwiftUICoreTests/Event/Gesture/LayoutGestureTests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,49 @@ struct LayoutGestureTests {
216216
}
217217
}
218218

219+
@Test(arguments: ResetTrigger.allCases)
220+
func childResetEvaluatesLazyDebugInputs(trigger: ResetTrigger) {
221+
withFixture(debugOutput: true) { fixture in
222+
let child = LazyDebugResponder(phase: trigger == .terminal ? .ended(()) : .active(()))
223+
fixture.setChildren([child])
224+
fixture.send(to: child)
225+
226+
if trigger != .terminal {
227+
#expect(fixture.outputs.phase.value.isActive)
228+
#expect(child.debugData == nil)
229+
switch trigger {
230+
case .terminal: break
231+
case .seed: fixture.resetSeed.value = 1
232+
case .removal: fixture.setChildren([])
233+
}
234+
}
235+
236+
let resetsBeforeEvaluation = child.resetCount
237+
let phase = fixture.outputs.phase.value
238+
switch trigger {
239+
case .terminal: #expect(phase.isEnded)
240+
case .seed: #expect(phase.isActive)
241+
case .removal: #expect(phase.isFailed)
242+
}
243+
#expect(child.resetCount - resetsBeforeEvaluation == 1)
244+
#expect(child.debugData?.resetSeed == (trigger == .seed ? 1 : 0))
245+
#expect(child.debugEventCount == 1)
246+
247+
if trigger == .terminal {
248+
let data = fixture.outputs.debugData!.value
249+
#expect(data.children.count == 1)
250+
#expect(data.children[0].phase.isEnded)
251+
#expect(data.children[0].resetSeed == 0)
252+
}
253+
}
254+
}
255+
256+
enum ResetTrigger: CaseIterable {
257+
case terminal
258+
case seed
259+
case removal
260+
}
261+
219262
@Test(arguments: [
220263
({ @Sendable in [] }, "failed"),
221264
({ @Sendable in [.failed, .failed] }, "failed"),
@@ -407,6 +450,56 @@ private final class TestResponder: ViewResponder {
407450
}
408451
}
409452

453+
private final class LazyDebugResponder: ViewResponder {
454+
let phase: GesturePhase<Void>
455+
var debugData: GestureDebug.Data?
456+
var debugEventCount: Int?
457+
var resetCount = 0
458+
459+
init(phase: GesturePhase<Void>) {
460+
self.phase = phase
461+
super.init()
462+
}
463+
464+
override func makeGesture(inputs: _GestureInputs) -> _GestureOutputs<Void> {
465+
var outputs = _GestureOutputs(phase: Attribute(value: phase))
466+
outputs.debugData = Attribute(Debug(
467+
responder: self,
468+
phase: outputs.phase,
469+
events: inputs.events,
470+
resetSeed: inputs.resetSeed
471+
))
472+
return outputs
473+
}
474+
475+
override func resetGesture() {
476+
resetCount += 1
477+
}
478+
479+
private struct Debug: Rule {
480+
let responder: LazyDebugResponder
481+
@Attribute var phase: GesturePhase<Void>
482+
@Attribute var events: [EventID: any EventType]
483+
@Attribute var resetSeed: UInt32
484+
485+
var value: GestureDebug.Data {
486+
let data = GestureDebug.Data(
487+
kind: .primitive,
488+
type: TestGesture.self,
489+
children: .init(),
490+
phase: phase,
491+
attribute: $phase.identifier,
492+
resetSeed: resetSeed,
493+
frame: .zero,
494+
properties: .init()
495+
)
496+
responder.debugEventCount = events.count
497+
responder.debugData = data
498+
return data
499+
}
500+
}
501+
}
502+
410503
private final class TestHost: ViewRendererHost, EventGraphHost {
411504
let viewGraph: ViewGraph
412505
let eventBindingManager = EventBindingManager()

0 commit comments

Comments
 (0)