Skip to content

Commit 28030ac

Browse files
dskuzadskuza
andcommitted
refactor(apple): always advance first frame (#11890) 1924060667
Co-authored-by: David Skuza <david@rive.app>
1 parent 673d253 commit 28030ac

File tree

4 files changed

+90
-44
lines changed

4 files changed

+90
-44
lines changed

.rive_head

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
68176685ac3e4fcd0ec1b06d90bd2c1137be8fc4
1+
1924060667de35317e19955dbfcdd629c1bffa76

Source/Experimental/View/RiveController.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import Combine
1111
@MainActor
1212
final class RiveController {
1313
let rive: Rive
14+
var isPaused = false {
15+
didSet {
16+
guard oldValue != isPaused else { return }
17+
if isPaused {
18+
resetTiming()
19+
}
20+
}
21+
}
1422

1523
private(set) var isSettled = false {
1624
didSet {
@@ -69,20 +77,19 @@ final class RiveController {
6977

7078
func advance(
7179
now: TimeInterval,
72-
isPaused: Bool,
7380
isOnscreen: Bool,
7481
drawableSize: CGSize,
7582
scaleProvider: ScaleProvider
7683
) -> RendererConfiguration? {
7784
/*
78-
| Condition | Advance? | Draw? |
79-
|-------------------------------------------------------|-----------------------------------------|-------|
80-
| Paused && hasProcessedFirstDraw | No | No |
81-
| First frame (hasProcessedFirstDraw == false) | Settled: No, Unsettled: Yes (delta 0) | Yes |
82-
| Settled && !becameOnscreen && !first frame | No | No |
83-
| Unsettled && offscreen && !first frame | Yes | No |
84-
| Unsettled && onscreen | Yes | Yes |
85-
| Settled && becameOnscreen | No | Yes |
85+
| Condition | Advance? | Draw? |
86+
|-------------------------------------------------------|----------------------------------|-------|
87+
| First frame (hasProcessedFirstDraw == false) | Yes (always, delta 0) | Yes |
88+
| Paused && hasProcessedFirstDraw | No | No |
89+
| Settled && !becameOnscreen && !first frame | No | No |
90+
| Unsettled && offscreen && !first frame | Yes | No |
91+
| Unsettled && onscreen | Yes | Yes |
92+
| Settled && becameOnscreen | No | Yes |
8693
*/
8794
// Track visibility transitions so settled views can redraw once when they return onscreen.
8895
let becameOnscreen = wasOnscreen == false && isOnscreen
@@ -108,6 +115,12 @@ final class RiveController {
108115
lastTimestamp = now
109116
}
110117

118+
let shouldAdvance = hasProcessedFirstDraw == false || isSettled == false
119+
if shouldAdvance {
120+
RiveLog.trace(tag: .view, "[RiveUIView] Advancing state machine (dt=\(delta))")
121+
rive.stateMachine.advance(by: delta)
122+
}
123+
111124
if isSettled {
112125
// Settled views do not animate, but we still allow:
113126
// 1) one bootstrap draw, and
@@ -116,10 +129,6 @@ final class RiveController {
116129
RiveLog.trace(tag: .view, "[RiveUIView] Skipping frame: settled with no onscreen transition")
117130
return nil
118131
}
119-
} else {
120-
// Unsettled views always advance, even if we may skip drawing this frame.
121-
RiveLog.trace(tag: .view, "[RiveUIView] Advancing state machine (dt=\(delta))")
122-
rive.stateMachine.advance(by: delta)
123132
}
124133

125134
// After the first draw, offscreen frames skip render output.

Source/Experimental/View/RiveUIView.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ public class RiveUIView: NativeView, MTKViewDelegate, ScaleProvider, DisplayLink
8787
private var _isPaused: Bool = true {
8888
didSet {
8989
let newValue = _isPaused
90-
if newValue {
91-
controller?.resetTiming()
92-
}
90+
controller?.isPaused = newValue
9391
#if !os(macOS) || RIVE_MAC_CATALYST
9492
displayLink?.isPaused = newValue
9593
#else
@@ -296,10 +294,7 @@ public class RiveUIView: NativeView, MTKViewDelegate, ScaleProvider, DisplayLink
296294
self?.bounds.size ?? .zero
297295
}
298296
)
299-
300-
if isPaused {
301-
controller?.resetTiming()
302-
}
297+
controller?.isPaused = isPaused
303298

304299
// If we are paused, we want to draw at least one frame
305300
// We'll leverage MTKView's (set)NeedsDisplay to draw once
@@ -346,7 +341,6 @@ public class RiveUIView: NativeView, MTKViewDelegate, ScaleProvider, DisplayLink
346341
let now = displayLink?.timestamp ?? CACurrentMediaTime()
347342
let configuration = controller.advance(
348343
now: now,
349-
isPaused: isPaused,
350344
isOnscreen: isOnscreen(),
351345
drawableSize: view.drawableSize,
352346
scaleProvider: self

Tests/Experimental/View/RiveControllerTests.swift

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,45 +70,44 @@ final class RiveControllerTests: XCTestCase {
7070
}
7171

7272
@MainActor
73-
func test_advance_whenSettledAndFirstDraw_returnsConfiguration_andDoesNotAdvance() async throws {
73+
func test_advance_whenSettledAndFirstDraw_returnsConfiguration_andAdvances() async throws {
7474
let fixture = try await makeController(dataBind: .none)
7575
await expectSettled(within: fixture)
7676

7777
let configuration = fixture.controller.advance(
7878
now: 10,
79-
isPaused: false,
8079
isOnscreen: true,
8180
drawableSize: CGSize(width: 100, height: 200),
8281
scaleProvider: MockScaleProvider()
8382
)
8483

8584
XCTAssertNotNil(configuration)
86-
XCTAssertTrue(fixture.commandQueue.advanceStateMachineCalls.isEmpty)
85+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls.count, 1)
86+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[0].time, 0)
8787
}
8888

8989
@MainActor
90-
func test_advance_whenSettledAfterFirstDraw_returnsNil_andDoesNotAdvance() async throws {
90+
func test_advance_whenSettledAfterFirstDraw_skipsSecondFrame_andOnlyFirstFrameAdvances() async throws {
9191
let fixture = try await makeController(dataBind: .none)
9292
await expectSettled(within: fixture)
9393

9494
let firstConfiguration = fixture.controller.advance(
9595
now: 10,
96-
isPaused: false,
9796
isOnscreen: true,
9897
drawableSize: CGSize(width: 100, height: 200),
9998
scaleProvider: MockScaleProvider()
10099
)
101100
let secondConfiguration = fixture.controller.advance(
102101
now: 10.5,
103-
isPaused: false,
104102
isOnscreen: true,
105103
drawableSize: CGSize(width: 100, height: 200),
106104
scaleProvider: MockScaleProvider()
107105
)
108106

109107
XCTAssertNotNil(firstConfiguration)
110108
XCTAssertNil(secondConfiguration)
111-
XCTAssertTrue(fixture.commandQueue.advanceStateMachineCalls.isEmpty)
109+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls.count, 1)
110+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[0].time, 0)
112111
}
113112

114113
@MainActor
@@ -117,7 +116,6 @@ final class RiveControllerTests: XCTestCase {
117116

118117
_ = fixture.controller.advance(
119118
now: 10,
120-
isPaused: false,
121119
isOnscreen: false,
122120
drawableSize: CGSize(width: 100, height: 200),
123121
scaleProvider: MockScaleProvider()
@@ -126,7 +124,6 @@ final class RiveControllerTests: XCTestCase {
126124

127125
let configuration = fixture.controller.advance(
128126
now: 10.5,
129-
isPaused: false,
130127
isOnscreen: true,
131128
drawableSize: CGSize(width: 100, height: 200),
132129
scaleProvider: MockScaleProvider()
@@ -143,7 +140,6 @@ final class RiveControllerTests: XCTestCase {
143140

144141
let configuration = fixture.controller.advance(
145142
now: 10,
146-
isPaused: false,
147143
isOnscreen: false,
148144
drawableSize: CGSize(width: 100, height: 200),
149145
scaleProvider: MockScaleProvider()
@@ -160,14 +156,12 @@ final class RiveControllerTests: XCTestCase {
160156

161157
let firstConfiguration = fixture.controller.advance(
162158
now: 10,
163-
isPaused: false,
164159
isOnscreen: false,
165160
drawableSize: CGSize(width: 100, height: 200),
166161
scaleProvider: MockScaleProvider()
167162
)
168163
let secondConfiguration = fixture.controller.advance(
169164
now: 10.5,
170-
isPaused: false,
171165
isOnscreen: false,
172166
drawableSize: CGSize(width: 100, height: 200),
173167
scaleProvider: MockScaleProvider()
@@ -186,14 +180,12 @@ final class RiveControllerTests: XCTestCase {
186180

187181
let firstConfiguration = fixture.controller.advance(
188182
now: 10,
189-
isPaused: false,
190183
isOnscreen: true,
191184
drawableSize: CGSize(width: 100, height: 200),
192185
scaleProvider: MockScaleProvider()
193186
)
194187
let secondConfiguration = fixture.controller.advance(
195188
now: 10.5,
196-
isPaused: false,
197189
isOnscreen: false,
198190
drawableSize: CGSize(width: 100, height: 200),
199191
scaleProvider: MockScaleProvider()
@@ -212,7 +204,6 @@ final class RiveControllerTests: XCTestCase {
212204

213205
let configuration = fixture.controller.advance(
214206
now: 10,
215-
isPaused: false,
216207
isOnscreen: true,
217208
drawableSize: CGSize(width: 640, height: 480),
218209
scaleProvider: MockScaleProvider()
@@ -229,14 +220,12 @@ final class RiveControllerTests: XCTestCase {
229220

230221
_ = fixture.controller.advance(
231222
now: 10,
232-
isPaused: false,
233223
isOnscreen: false,
234224
drawableSize: CGSize(width: 100, height: 200),
235225
scaleProvider: MockScaleProvider()
236226
)
237227
_ = fixture.controller.advance(
238228
now: 10.5,
239-
isPaused: false,
240229
isOnscreen: false,
241230
drawableSize: CGSize(width: 100, height: 200),
242231
scaleProvider: MockScaleProvider()
@@ -250,17 +239,16 @@ final class RiveControllerTests: XCTestCase {
250239
@MainActor
251240
func test_advance_whenPaused_allowsFirstDraw_thenBlocksSubsequentDraws() async throws {
252241
let fixture = try await makeController(dataBind: .none)
242+
fixture.controller.isPaused = true
253243

254244
let firstConfiguration = fixture.controller.advance(
255245
now: 10,
256-
isPaused: true,
257246
isOnscreen: true,
258247
drawableSize: CGSize(width: 100, height: 200),
259248
scaleProvider: MockScaleProvider()
260249
)
261250
let secondConfiguration = fixture.controller.advance(
262251
now: 10.5,
263-
isPaused: true,
264252
isOnscreen: true,
265253
drawableSize: CGSize(width: 100, height: 200),
266254
scaleProvider: MockScaleProvider()
@@ -275,25 +263,24 @@ final class RiveControllerTests: XCTestCase {
275263
@MainActor
276264
func test_advance_whenResumedAfterPausedBlock_usesZeroDeltaFirstFrame_withoutResetTiming() async throws {
277265
let fixture = try await makeController(dataBind: .none)
266+
fixture.controller.isPaused = true
278267

279268
_ = fixture.controller.advance(
280269
now: 10,
281-
isPaused: true,
282270
isOnscreen: true,
283271
drawableSize: CGSize(width: 100, height: 200),
284272
scaleProvider: MockScaleProvider()
285273
)
286274
_ = fixture.controller.advance(
287275
now: 10.5,
288-
isPaused: true,
289276
isOnscreen: true,
290277
drawableSize: CGSize(width: 100, height: 200),
291278
scaleProvider: MockScaleProvider()
292279
)
293280

281+
fixture.controller.isPaused = false
294282
_ = fixture.controller.advance(
295283
now: 11,
296-
isPaused: false,
297284
isOnscreen: false,
298285
drawableSize: CGSize(width: 100, height: 200),
299286
scaleProvider: MockScaleProvider()
@@ -304,6 +291,62 @@ final class RiveControllerTests: XCTestCase {
304291
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[1].time, 0)
305292
}
306293

294+
@MainActor
295+
func test_advance_whenResumedAfterPauseTransition_usesZeroDeltaEvenIfTimestampWasSet() async throws {
296+
let fixture = try await makeController(dataBind: .none)
297+
298+
_ = fixture.controller.advance(
299+
now: 10,
300+
isOnscreen: true,
301+
drawableSize: CGSize(width: 100, height: 200),
302+
scaleProvider: MockScaleProvider()
303+
)
304+
fixture.controller.isPaused = true
305+
_ = fixture.controller.advance(
306+
now: 20,
307+
isOnscreen: true,
308+
drawableSize: CGSize(width: 100, height: 200),
309+
scaleProvider: MockScaleProvider()
310+
)
311+
fixture.controller.isPaused = false
312+
_ = fixture.controller.advance(
313+
now: 30,
314+
isOnscreen: true,
315+
drawableSize: CGSize(width: 100, height: 200),
316+
scaleProvider: MockScaleProvider()
317+
)
318+
319+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls.count, 2)
320+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[0].time, 0)
321+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[1].time, 0)
322+
}
323+
324+
@MainActor
325+
func test_settingIsPausedTrue_resetsTimingForNextUnpausedAdvance() async throws {
326+
let fixture = try await makeController(dataBind: .none)
327+
328+
_ = fixture.controller.advance(
329+
now: 10,
330+
isOnscreen: true,
331+
drawableSize: CGSize(width: 100, height: 200),
332+
scaleProvider: MockScaleProvider()
333+
)
334+
335+
fixture.controller.isPaused = true
336+
fixture.controller.isPaused = false
337+
338+
_ = fixture.controller.advance(
339+
now: 30,
340+
isOnscreen: true,
341+
drawableSize: CGSize(width: 100, height: 200),
342+
scaleProvider: MockScaleProvider()
343+
)
344+
345+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls.count, 2)
346+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[0].time, 0)
347+
XCTAssertEqual(fixture.commandQueue.advanceStateMachineCalls[1].time, 0)
348+
}
349+
307350
// MARK: - Helpers
308351

309352
@MainActor

0 commit comments

Comments
 (0)