Submission checklist
Description
Description
We are seeing an intermittent production crash from Crashlytics when a RiveUIView used as a launch screen animation is removed and released.
The crash happens on Rive's background command server thread during teardown:
Crashed: app.rive.command-server
0 libdispatch.dylib _dispatch_semaphore_dispose.cold.1
1 libdispatch.dylib _dispatch_semaphore_signal_slow
2 libdispatch.dylib _dispatch_dispose
3 RiveRuntime RiveHitResultFromRuntime(rive::HitResult)
...
11 RiveRuntime rive::CommandServer::~CommandServer()
12 RiveRuntime rive::CommandServer::~CommandServer()
This may be related to other lifecycle / teardown issues in the new async runtime, especially:
However, this crash stack looks different because it happens during rive::CommandServer destruction, with _dispatch_semaphore_dispose / _dispatch_semaphore_signal_slow at the top of the crashed thread.
Previous working version
No response
Rive Apple runtime version
6.19.0
Reproduction steps / code
Reproduction steps / code
We have not been able to reproduce this deterministically in a small sample app yet. The production crash happens during launch-screen teardown, when a temporary RiveUIView is removed from the view hierarchy and the owning view is released.
The relevant usage pattern is:
- Create a temporary UIKit launch screen view.
- Inside that view, create a
RiveUIView(rive: nil).
- Load a local
.riv file using the new async runtime:
Worker
File
Artboard
StateMachine
ViewModelInstance
Rive
- Assign the resulting
Rive instance to riveView.rive.
- After the launch flow completes, remove the launch screen from the view hierarchy.
- Immediately release the owning launch screen view.
- Crashlytics intermittently reports a crash on
app.rive.command-server during rive::CommandServer teardown.
Reduced version of the production code:
import UIKit
import RiveRuntime
final class LaunchScreenView: UIView {
private let phaseProperty = NumberProperty(path: "phase")
private var launchViewModelInstance: ViewModelInstance?
private var riveLoadTask: Task<Void, Never>?
private lazy var riveView: RiveUIView = {
let view = RiveUIView(rive: nil)
view.backgroundColor = .clear
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(riveView)
riveView.frame = bounds
riveView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
loadRiveAnimation()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
riveLoadTask?.cancel()
}
private func loadRiveAnimation() {
riveLoadTask = Task { [weak self] in
await self?.prepareRiveAnimation()
}
}
@MainActor
private func prepareRiveAnimation() async {
do {
let worker = try await Worker()
let file = try await File(source: .local("launch", Bundle.main), worker: worker)
let artboard = try await file.createArtboard()
let stateMachine = try await artboard.createStateMachine("LaunchStateMachine")
let viewModelInstance = try await file.createViewModelInstance(
.blank(from: .name("LaunchViewModel"))
)
viewModelInstance.setValue(of: phaseProperty, to: 1)
let rive = try await Rive(
file: file,
artboard: artboard,
stateMachine: stateMachine,
dataBind: .instance(viewModelInstance),
fit: .contain(alignment: .center)
)
launchViewModelInstance = viewModelInstance
riveView.rive = rive
} catch {
print("Failed to load Rive launch animation: \(error)")
}
}
@MainActor
func updatePhase(_ phase: Float) {
launchViewModelInstance?.setValue(of: phaseProperty, to: phase)
}
}
The launch view is owned by a UITabBarController and released immediately after the launch transition finishes:
final class RootTabBarController: UITabBarController {
private var launchScreenView: LaunchScreenView? = LaunchScreenView()
func startLaunchTransition() {
guard let launchScreenView else { return }
view.addSubview(launchScreenView)
launchScreenView.frame = view.bounds
launchScreenView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
launchScreenView.updatePhase(1)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) { [weak self] in
self?.finishLaunchTransition()
}
}
private func finishLaunchTransition() {
guard let launchScreenView else { return }
UIView.animate(withDuration: 0.25) {
launchScreenView.alpha = 0
} completion: { [weak self] _ in
launchScreenView.removeFromSuperview()
// Releasing this temporary view releases RiveUIView / Rive / Worker.
// Crashlytics intermittently reports a crash shortly after this,
// on the `app.rive.command-server` thread.
self?.launchScreenView = nil
}
}
}
The Crashlytics stack suggests the crash happens after this release path, while RiveRuntime is tearing down the worker command server:
Crashed: app.rive.command-server
0 libdispatch.dylib _dispatch_semaphore_dispose.cold.1
1 libdispatch.dylib _dispatch_semaphore_signal_slow
2 libdispatch.dylib _dispatch_dispose
3 RiveRuntime RiveHitResultFromRuntime(rive::HitResult)
...
11 RiveRuntime rive::CommandServer::~CommandServer()
12 RiveRuntime rive::CommandServer::~CommandServer()
### Upload your reproduction files / stack trace
_No response_
### Source `.riv` / `.rev` file
_No response_
### Screenshots / video
_No response_
### Device
iPad (11th generation),iPhone 11,iPhone XS Max
### Apple OS version
iPadOS 26.2.0,iOS 16.1.1,iOS 17.1.2,iOS 15.8.4
### Additional context
_No response_
Submission checklist
RiveRuntimeDescription
Description
We are seeing an intermittent production crash from Crashlytics when a
RiveUIViewused as a launch screen animation is removed and released.The crash happens on Rive's background command server thread during teardown:
This may be related to other lifecycle / teardown issues in the new async runtime, especially:
EXC_BAD_ACCESSinCommandQueue::processMessages()during teardown aftersetValueWorkerstays aliveWorkeracross multipleRiveinstancesHowever, this crash stack looks different because it happens during
rive::CommandServerdestruction, with_dispatch_semaphore_dispose/_dispatch_semaphore_signal_slowat the top of the crashed thread.Previous working version
No response
Rive Apple runtime version
6.19.0
Reproduction steps / code
Reproduction steps / code
We have not been able to reproduce this deterministically in a small sample app yet. The production crash happens during launch-screen teardown, when a temporary
RiveUIViewis removed from the view hierarchy and the owning view is released.The relevant usage pattern is:
RiveUIView(rive: nil)..rivfile using the new async runtime:WorkerFileArtboardStateMachineViewModelInstanceRiveRiveinstance toriveView.rive.app.rive.command-serverduringrive::CommandServerteardown.Reduced version of the production code:
The launch view is owned by a
UITabBarControllerand released immediately after the launch transition finishes:The Crashlytics stack suggests the crash happens after this release path, while RiveRuntime is tearing down the worker command server: