Skip to content

Commit cf9a8bb

Browse files
mfazekasclaude
andcommitted
fix(ios): prevent race condition in referenced asset loading
Fixed race condition where views would refresh before asset downloads completed. This mirrors the Android fix in commit c5adec2. Changes: - ReferencedAssetLoader: Added completion handlers to entire async chain - Completion fires in both success and failure cases - HybridRiveFile: Use DispatchGroup to wait for all asset loads - refreshAfterAssetChange() only called after all downloads finish This ensures views display updated assets even with slow network. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 253249a commit cf9a8bb

2 files changed

Lines changed: 56 additions & 26 deletions

File tree

ios/HybridRiveFile.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,26 @@ class HybridRiveFile: HybridRiveFileSpec {
7676
return
7777
}
7878

79+
let dispatchGroup = DispatchGroup()
7980
var hasChanged = false
81+
8082
for (key, assetData) in assetsData {
8183
guard let asset = cache[key] else { continue }
8284
if let riveFactory = cachedFactory {
83-
loader.loadAsset(source: assetData, asset: asset, factory: riveFactory)
85+
dispatchGroup.enter()
86+
loader.loadAsset(source: assetData, asset: asset, factory: riveFactory) {
87+
dispatchGroup.leave()
88+
}
8489
} else {
8590
RCTLogError("[RiveFile] no factory available for update")
8691
}
8792
hasChanged = true
8893
}
8994

9095
if hasChanged {
91-
refreshAfterAssetChange()
96+
dispatchGroup.notify(queue: .main) { [weak self] in
97+
self?.refreshAfterAssetChange()
98+
}
9299
}
93100
}
94101

ios/ReferencedAssetLoader.swift

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ final class ReferencedAssetLoader {
4141
handleRiveError(error: createIncorrectRiveURL(url))
4242
}
4343

44-
private func downloadUrlAsset(url: String, listener: @escaping (Data) -> Void) {
44+
private func downloadUrlAsset(url: String, listener: @escaping (Data) -> Void, onError: @escaping () -> Void) {
4545
guard isValidUrl(url) else {
4646
handleInvalidUrlError(url: url)
47+
onError()
4748
return
4849
}
4950
if let fileUrl = URL(string: url), fileUrl.scheme == "file" {
@@ -52,30 +53,36 @@ final class ReferencedAssetLoader {
5253
listener(data)
5354
} catch {
5455
handleInvalidUrlError(url: url)
56+
onError()
5557
}
5658
return
5759
}
5860

5961
let queue = URLSession.shared
6062
guard let requestUrl = URL(string: url) else {
6163
handleInvalidUrlError(url: url)
64+
onError()
6265
return
6366
}
6467

6568
let request = URLRequest(url: requestUrl)
6669
let task = queue.dataTask(with: request) { [weak self] data, response, error in
6770
if error != nil {
6871
self?.handleInvalidUrlError(url: url)
72+
onError()
6973
} else if let data = data {
7074
listener(data)
75+
} else {
76+
onError()
7177
}
7278
}
7379

7480
task.resume()
7581
}
7682

77-
private func processAssetBytes(_ data: Data, asset: RiveFileAsset, factory: RiveFactory) {
83+
private func processAssetBytes(_ data: Data, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void) {
7884
if data.isEmpty == true {
85+
completion()
7986
return
8087
}
8188
DispatchQueue.global(qos: .background).async {
@@ -84,39 +91,50 @@ final class ReferencedAssetLoader {
8491
let decodedImage = factory.decodeImage(data)
8592
DispatchQueue.main.async {
8693
imageAsset.renderImage(decodedImage)
94+
completion()
8795
}
8896
case let fontAsset as RiveFontAsset:
8997
let decodedFont = factory.decodeFont(data)
9098
DispatchQueue.main.async {
9199
fontAsset.font(decodedFont)
100+
completion()
92101
}
93102
case let audioAsset as RiveAudioAsset:
94-
guard let decodedAudio = factory.decodeAudio(data) else { return }
103+
guard let decodedAudio = factory.decodeAudio(data) else {
104+
DispatchQueue.main.async {
105+
completion()
106+
}
107+
return
108+
}
95109
DispatchQueue.main.async {
96110
audioAsset.audio(decodedAudio)
111+
completion()
97112
}
98113
default:
99-
break
114+
DispatchQueue.main.async {
115+
completion()
116+
}
100117
}
101118
}
102119
}
103120

104121
private func handleSourceAssetId(
105-
_ sourceAssetId: String, asset: RiveFileAsset, factory: RiveFactory
122+
_ sourceAssetId: String, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void
106123
) {
107124
guard URL(string: sourceAssetId) != nil else {
125+
completion()
108126
return
109127
}
110128

111-
downloadUrlAsset(url: sourceAssetId) { [weak self] data in
112-
self?.processAssetBytes(data, asset: asset, factory: factory)
113-
}
129+
downloadUrlAsset(url: sourceAssetId, listener: { [weak self] data in
130+
self?.processAssetBytes(data, asset: asset, factory: factory, completion: completion)
131+
}, onError: completion)
114132
}
115133

116-
private func handleSourceUrl(_ sourceUrl: String, asset: RiveFileAsset, factory: RiveFactory) {
117-
downloadUrlAsset(url: sourceUrl) { [weak self] data in
118-
self?.processAssetBytes(data, asset: asset, factory: factory)
119-
}
134+
private func handleSourceUrl(_ sourceUrl: String, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void) {
135+
downloadUrlAsset(url: sourceUrl, listener: { [weak self] data in
136+
self?.processAssetBytes(data, asset: asset, factory: factory, completion: completion)
137+
}, onError: completion)
120138
}
121139

122140
private func splitFileNameAndExtension(fileName: String) -> (name: String?, ext: String?)? {
@@ -128,18 +146,20 @@ final class ReferencedAssetLoader {
128146
}
129147

130148
private func loadResourceAsset(
131-
sourceAsset: String, path: String?, listener: @escaping (Data) -> Void
149+
sourceAsset: String, path: String?, listener: @escaping (Data) -> Void, onError: @escaping () -> Void
132150
) {
133151
guard let splitSourceAssetName = splitFileNameAndExtension(fileName: sourceAsset),
134152
let name = splitSourceAssetName.name,
135153
let ext = splitSourceAssetName.ext
136154
else {
137155
handleRiveError(error: createAssetFileError(sourceAsset))
156+
onError()
138157
return
139158
}
140159

141160
guard let folderUrl = Bundle.main.url(forResource: name, withExtension: ext) else {
142161
handleRiveError(error: createAssetFileError(sourceAsset))
162+
onError()
143163
return
144164
}
145165

@@ -152,39 +172,42 @@ final class ReferencedAssetLoader {
152172
} catch {
153173
DispatchQueue.main.async {
154174
self?.handleRiveError(error: createAssetFileError(sourceAsset))
175+
onError()
155176
}
156177
}
157178
}
158179
}
159180

160181
private func handleSourceAsset(
161-
_ sourceAsset: String, path: String?, asset: RiveFileAsset, factory: RiveFactory
182+
_ sourceAsset: String, path: String?, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void
162183
) {
163-
loadResourceAsset(sourceAsset: sourceAsset, path: path) { [weak self] data in
164-
self?.processAssetBytes(data, asset: asset, factory: factory)
165-
}
184+
loadResourceAsset(sourceAsset: sourceAsset, path: path, listener: { [weak self] data in
185+
self?.processAssetBytes(data, asset: asset, factory: factory, completion: completion)
186+
}, onError: completion)
166187
}
167188

168189
private func loadAssetInternal(
169-
source: ResolvedReferencedAsset, asset: RiveFileAsset, factory: RiveFactory
190+
source: ResolvedReferencedAsset, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void
170191
) {
171192
let sourceAssetId = source.sourceAssetId
172193
let sourceUrl = source.sourceUrl
173194
let sourceAsset = source.sourceAsset
174195

175196
if let sourceAssetId = sourceAssetId {
176-
handleSourceAssetId(sourceAssetId, asset: asset, factory: factory)
197+
handleSourceAssetId(sourceAssetId, asset: asset, factory: factory, completion: completion)
177198
} else if let sourceUrl = sourceUrl {
178-
handleSourceUrl(sourceUrl, asset: asset, factory: factory)
199+
handleSourceUrl(sourceUrl, asset: asset, factory: factory, completion: completion)
179200
} else if let sourceAsset = sourceAsset {
180-
handleSourceAsset(sourceAsset, path: source.path, asset: asset, factory: factory)
201+
handleSourceAsset(sourceAsset, path: source.path, asset: asset, factory: factory, completion: completion)
202+
} else {
203+
completion()
181204
}
182205
}
183206

184207
func loadAsset(
185-
source: ResolvedReferencedAsset, asset: RiveFileAsset, factory: RiveFactory
208+
source: ResolvedReferencedAsset, asset: RiveFileAsset, factory: RiveFactory, completion: @escaping () -> Void
186209
) {
187-
loadAssetInternal(source: source, asset: asset, factory: factory)
210+
loadAssetInternal(source: source, asset: asset, factory: factory, completion: completion)
188211
}
189212

190213
func createCustomLoader(referencedAssets: ReferencedAssetsType?, cache: SendableRef<ReferencedAssetCache>, factory factoryOut: SendableRef<RiveFactory?>)
@@ -204,7 +227,7 @@ final class ReferencedAssetLoader {
204227
cache.value[asset.uniqueName()] = asset
205228
factoryOut.value = factory
206229

207-
self.loadAssetInternal(source: assetData, asset: asset, factory: factory)
230+
self.loadAssetInternal(source: assetData, asset: asset, factory: factory, completion: {})
208231

209232
return false
210233
}

0 commit comments

Comments
 (0)