From 1d3071b123bdf3d3110510a64589444d09f2392f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 20 May 2026 08:55:15 +0200 Subject: [PATCH 1/4] chore(ios): upgrade rive-ios SDK from 6.18.2 to 6.20.4 Includes render context retain fix, race condition fixes, and concurrency improvements that should address #225. --- example/ios/Podfile.lock | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6e16c751..782dd595 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1357,7 +1357,7 @@ PODS: - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - react-native-safe-area-context (5.6.2): + - react-native-safe-area-context (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1372,8 +1372,8 @@ PODS: - React-hermes - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.6.2) - - react-native-safe-area-context/fabric (= 5.6.2) + - react-native-safe-area-context/common (= 5.7.0) + - react-native-safe-area-context/fabric (= 5.7.0) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -1383,7 +1383,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context/common (5.6.2): + - react-native-safe-area-context/common (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1407,7 +1407,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context/fabric (5.6.2): + - react-native-safe-area-context/fabric (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1754,7 +1754,7 @@ PODS: - React-logger (= 0.79.2) - React-perflogger (= 0.79.2) - React-utils (= 0.79.2) - - RiveRuntime (6.18.2) + - RiveRuntime (6.20.4) - RNCAsyncStorage (2.2.0): - DoubleConversion - glog @@ -1904,7 +1904,7 @@ PODS: - ReactCommon/turbomodule/core - RNWorklets - Yoga - - RNRive (0.4.2): + - RNRive (0.4.7): - DoubleConversion - glog - hermes-engine @@ -1928,7 +1928,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RiveRuntime (= 6.18.2) + - RiveRuntime (= 6.20.4) - Yoga - RNScreens (4.18.0): - DoubleConversion @@ -2347,7 +2347,7 @@ SPEC CHECKSUMS: React-logger: 8edfcedc100544791cd82692ca5a574240a16219 React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468 React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6 - react-native-safe-area-context: 0b8555c40461feb7198e999912a3446602e7c601 + react-native-safe-area-context: 26634d9b636a98ceee20cb6fa5dc946922f1e90f React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d @@ -2379,12 +2379,12 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584 ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0 - RiveRuntime: 55c7a7badd9a8389d20fc8a75b7c6accc851b69a + RiveRuntime: f99ddd1d4b6a420dea5943ff52502e8f92ae7a92 RNCAsyncStorage: a1c8cc8a99c32de1244a9cf707bf9d83d0de0f71 RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5 RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344 - RNRive: c02b3545abcf477d074945c5103f9f4bc9d8d672 + RNRive: 042cbd7de1e5dd7e11aac076ade185926717543b RNScreens: f38464ec1e83bda5820c3b05ccf4908e3841c5cc RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 diff --git a/package.json b/package.json index be366390..62f946aa 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ }, "homepage": "https://github.com/rive-app/rive-nitro-react-native#readme", "runtimeVersions": { - "ios": "6.18.2", + "ios": "6.20.4", "android": "11.4.0" }, "publishConfig": { From 341b51d92dbe83a54f0492e2cbbae6cac9a1cfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 20 May 2026 09:09:48 +0200 Subject: [PATCH 2/4] fix(ios): serialize RiveFactory decode calls to prevent EXC_BAD_ACCESS (#225) RiveFactory.decodeImage/decodeFont/decodeAudio are not thread-safe. Using DispatchQueue.global (concurrent) caused crashes when multiple assets decoded simultaneously. Replaced with a dedicated serial queue. --- ios/ReferencedAssetLoader.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/ReferencedAssetLoader.swift b/ios/ReferencedAssetLoader.swift index 892b8ef0..ccc1003e 100644 --- a/ios/ReferencedAssetLoader.swift +++ b/ios/ReferencedAssetLoader.swift @@ -11,6 +11,7 @@ func createAssetFileError(_ assetName: String) -> NitroRiveError { } final class ReferencedAssetLoader { + private static let decodeQueue = DispatchQueue(label: "com.rive.asset-decode") private var activeLoadCount = 0 private var activeFileRef: RiveFile? @@ -42,7 +43,7 @@ final class ReferencedAssetLoader { completion() return } - DispatchQueue.global(qos: .background).async { + Self.decodeQueue.async { switch asset { case let imageAsset as RiveImageAsset: let decodedImage = factory.decodeImage(data) From 93efcf129aad8cb3107daace05b6d03550cd1305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 20 May 2026 13:54:28 +0200 Subject: [PATCH 3/4] fix(ios): decode images on main thread, keep file alive during decode decodeImage runs through RenderContext which is not thread-safe and races with beginFrame/endFrame on the main thread. Move it to run synchronously within the existing MainActor context. For font/audio (thread-safe), keep background decode but capture self strongly so the ReferencedAssetLoader (and its activeFileRef) stays alive until the dispatch completes. --- ios/ReferencedAssetLoader.swift | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ios/ReferencedAssetLoader.swift b/ios/ReferencedAssetLoader.swift index ccc1003e..7b3954c5 100644 --- a/ios/ReferencedAssetLoader.swift +++ b/ios/ReferencedAssetLoader.swift @@ -43,36 +43,36 @@ final class ReferencedAssetLoader { completion() return } - Self.decodeQueue.async { - switch asset { - case let imageAsset as RiveImageAsset: - let decodedImage = factory.decodeImage(data) - DispatchQueue.main.async { - imageAsset.renderImage(decodedImage) - completion() - } - case let fontAsset as RiveFontAsset: + switch asset { + case let imageAsset as RiveImageAsset: + // decodeImage is not thread-safe — decode on main thread, synchronously + // within the current MainActor context (caller is already on main). + let decodedImage = factory.decodeImage(data) + imageAsset.renderImage(decodedImage) + completion() + case let fontAsset as RiveFontAsset: + Self.decodeQueue.async { [self] in let decodedFont = factory.decodeFont(data) DispatchQueue.main.async { fontAsset.font(decodedFont) completion() + _ = self } - case let audioAsset as RiveAudioAsset: + } + case let audioAsset as RiveAudioAsset: + Self.decodeQueue.async { [self] in guard let decodedAudio = factory.decodeAudio(data) else { - DispatchQueue.main.async { - completion() - } + DispatchQueue.main.async { completion() } return } DispatchQueue.main.async { audioAsset.audio(decodedAudio) completion() - } - default: - DispatchQueue.main.async { - completion() + _ = self } } + default: + completion() } } From 2339a93a2959c790c4219a552d3944a537dfe606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 20 May 2026 16:16:48 +0200 Subject: [PATCH 4/4] refactor(ios): extract decodeAndApply helper for asset decode logic --- ios/ReferencedAssetLoader.swift | 68 +++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/ios/ReferencedAssetLoader.swift b/ios/ReferencedAssetLoader.swift index 7b3954c5..43825772 100644 --- a/ios/ReferencedAssetLoader.swift +++ b/ios/ReferencedAssetLoader.swift @@ -36,41 +36,59 @@ final class ReferencedAssetLoader { RCTLogError("\(error)") } + /// Decodes an asset and applies the result on the main thread. + /// + /// - `onMain: true` — decode + apply run synchronously on the caller's + /// thread (must already be main). Used for `decodeImage` which is not + /// thread-safe. + /// - `onMain: false` — decode runs on a serial background queue, then + /// apply + completion dispatch to main. The `[self]` capture keeps the + /// `ReferencedAssetLoader` (and its `activeFileRef`) alive until the + /// main-thread block completes, preventing use-after-free on the factory. + private func decodeAndApply( + onMain: Bool, + decode: @escaping () -> T?, + apply: @escaping (T) -> Void, + completion: @escaping () -> Void + ) { + if onMain { + if let result = decode() { apply(result) } + completion() + } else { + Self.decodeQueue.async { [self] in + let result = decode() + DispatchQueue.main.async { + if let result { apply(result) } + completion() + _ = self + } + } + } + } + private func processAssetBytes( _ data: Data, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void ) { - if data.isEmpty == true { + if data.isEmpty { completion() return } switch asset { case let imageAsset as RiveImageAsset: - // decodeImage is not thread-safe — decode on main thread, synchronously - // within the current MainActor context (caller is already on main). - let decodedImage = factory.decodeImage(data) - imageAsset.renderImage(decodedImage) - completion() + decodeAndApply(onMain: true, + decode: { factory.decodeImage(data) }, + apply: { imageAsset.renderImage($0) }, + completion: completion) case let fontAsset as RiveFontAsset: - Self.decodeQueue.async { [self] in - let decodedFont = factory.decodeFont(data) - DispatchQueue.main.async { - fontAsset.font(decodedFont) - completion() - _ = self - } - } + decodeAndApply(onMain: false, + decode: { factory.decodeFont(data) }, + apply: { fontAsset.font($0) }, + completion: completion) case let audioAsset as RiveAudioAsset: - Self.decodeQueue.async { [self] in - guard let decodedAudio = factory.decodeAudio(data) else { - DispatchQueue.main.async { completion() } - return - } - DispatchQueue.main.async { - audioAsset.audio(decodedAudio) - completion() - _ = self - } - } + decodeAndApply(onMain: false, + decode: { factory.decodeAudio(data) }, + apply: { audioAsset.audio($0) }, + completion: completion) default: completion() }