From bace9069a593885c71ca16e2315fc5cf807b68ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 27 Nov 2025 15:22:52 +0100 Subject: [PATCH] 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. --- ios/RiveReactNativeView.swift | 44 +++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/ios/RiveReactNativeView.swift b/ios/RiveReactNativeView.swift index 381a8201..d97926e3 100644 --- a/ios/RiveReactNativeView.swift +++ b/ios/RiveReactNativeView.swift @@ -30,6 +30,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate var viewModel: RiveViewModel? var dataBindingViewModelInstance: RiveDataBindingViewModel.Instance? var cachedRiveFactory: RiveFactory? + var cachedRiveFile: RiveFile? var previousReferencedAssets: NSDictionary? var cachedFileAssets: [String: RiveFileAsset] = [:] @@ -158,6 +159,10 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate private func cleanupFileAssetCache() { cachedFileAssets.removeAll() cachedRiveFactory = nil + // Note: We intentionally don't clear cachedRiveFile here to prevent a race condition + // where async asset loaders (both custom and CDN) may still be using the factory + // tied to the RiveFile. The cachedRiveFile will be replaced on next load or + // released when this view is deallocated. } override func layoutSubviews() { @@ -331,6 +336,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } else { updatedViewModel = RiveViewModel(fileName: name, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, customLoader: customLoader) } + cachedRiveFile = updatedViewModel.riveModel?.riveFile warnForUnusedAssets() updatedViewModel.layoutScaleFactor = layoutScaleFactor.doubleValue @@ -355,6 +361,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } do { let riveFile = try RiveFile(data: data, loadCdn: true, customAssetLoader: customLoader) + self.cachedRiveFile = riveFile let riveModel = RiveModel(riveFile: riveFile) let fit = self.convertFit(self.fit) let alignment = self.convertAlignment(self.alignment) @@ -514,25 +521,28 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate if (data.isEmpty == true) { return; } + let riveFileRef = cachedRiveFile DispatchQueue.global(qos: .background).async { - switch asset { - case let imageAsset as RiveImageAsset: - let decodedImage = factory.decodeImage(data) - DispatchQueue.main.async { - imageAsset.renderImage(decodedImage) - } - case let fontAsset as RiveFontAsset: - let decodedFont = factory.decodeFont(data) - DispatchQueue.main.async { - fontAsset.font(decodedFont) - } - case let audioAsset as RiveAudioAsset: - guard let decodedAudio = factory.decodeAudio(data) else { return } - DispatchQueue.main.async { - audioAsset.audio(decodedAudio) + withExtendedLifetime(riveFileRef) { + switch asset { + case let imageAsset as RiveImageAsset: + let decodedImage = factory.decodeImage(data) + DispatchQueue.main.async { + imageAsset.renderImage(decodedImage) + } + case let fontAsset as RiveFontAsset: + let decodedFont = factory.decodeFont(data) + DispatchQueue.main.async { + fontAsset.font(decodedFont) + } + case let audioAsset as RiveAudioAsset: + guard let decodedAudio = factory.decodeAudio(data) else { return } + DispatchQueue.main.async { + audioAsset.audio(decodedAudio) + } + default: + break } - default: - break } } }