Skip to content

Commit 9f8b0bf

Browse files
committed
fix(ios): prevent crash when view is disposed during async asset loading
Keep RiveFile alive during async asset loading operations to prevent factory invalidation. The cachedRiveFile reference is retained until the next load or view deallocation, ensuring the RiveFactory remains valid for background decode operations.
1 parent f4bb765 commit 9f8b0bf

File tree

1 file changed

+27
-17
lines changed

1 file changed

+27
-17
lines changed

ios/RiveReactNativeView.swift

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
3030
var viewModel: RiveViewModel?
3131
var dataBindingViewModelInstance: RiveDataBindingViewModel.Instance?
3232
var cachedRiveFactory: RiveFactory?
33+
var cachedRiveFile: RiveFile?
3334
var previousReferencedAssets: NSDictionary?
3435
var cachedFileAssets: [String: RiveFileAsset] = [:]
3536

@@ -158,6 +159,10 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
158159
private func cleanupFileAssetCache() {
159160
cachedFileAssets.removeAll()
160161
cachedRiveFactory = nil
162+
// Note: We intentionally don't clear cachedRiveFile here to prevent a race condition
163+
// where async asset loaders (both custom and CDN) may still be using the factory
164+
// tied to the RiveFile. The cachedRiveFile will be replaced on next load or
165+
// released when this view is deallocated.
161166
}
162167

163168
override func layoutSubviews() {
@@ -331,6 +336,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
331336
} else {
332337
updatedViewModel = RiveViewModel(fileName: name, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, customLoader: customLoader)
333338
}
339+
cachedRiveFile = updatedViewModel.riveModel?.riveFile
334340
warnForUnusedAssets()
335341

336342
updatedViewModel.layoutScaleFactor = layoutScaleFactor.doubleValue
@@ -355,6 +361,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
355361
}
356362
do {
357363
let riveFile = try RiveFile(data: data, loadCdn: true, customAssetLoader: customLoader)
364+
self.cachedRiveFile = riveFile
358365
let riveModel = RiveModel(riveFile: riveFile)
359366
let fit = self.convertFit(self.fit)
360367
let alignment = self.convertAlignment(self.alignment)
@@ -514,25 +521,28 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
514521
if (data.isEmpty == true) {
515522
return;
516523
}
524+
let riveFileRef = cachedRiveFile
517525
DispatchQueue.global(qos: .background).async {
518-
switch asset {
519-
case let imageAsset as RiveImageAsset:
520-
let decodedImage = factory.decodeImage(data)
521-
DispatchQueue.main.async {
522-
imageAsset.renderImage(decodedImage)
523-
}
524-
case let fontAsset as RiveFontAsset:
525-
let decodedFont = factory.decodeFont(data)
526-
DispatchQueue.main.async {
527-
fontAsset.font(decodedFont)
528-
}
529-
case let audioAsset as RiveAudioAsset:
530-
guard let decodedAudio = factory.decodeAudio(data) else { return }
531-
DispatchQueue.main.async {
532-
audioAsset.audio(decodedAudio)
526+
withExtendedLifetime(riveFileRef) {
527+
switch asset {
528+
case let imageAsset as RiveImageAsset:
529+
let decodedImage = factory.decodeImage(data)
530+
DispatchQueue.main.async {
531+
imageAsset.renderImage(decodedImage)
532+
}
533+
case let fontAsset as RiveFontAsset:
534+
let decodedFont = factory.decodeFont(data)
535+
DispatchQueue.main.async {
536+
fontAsset.font(decodedFont)
537+
}
538+
case let audioAsset as RiveAudioAsset:
539+
guard let decodedAudio = factory.decodeAudio(data) else { return }
540+
DispatchQueue.main.async {
541+
audioAsset.audio(decodedAudio)
542+
}
543+
default:
544+
break
533545
}
534-
default:
535-
break
536546
}
537547
}
538548
}

0 commit comments

Comments
 (0)