Skip to content

Commit 3bbae70

Browse files
committed
feat: add out of band assets ios
1 parent d84dd0c commit 3bbae70

File tree

3 files changed

+169
-5
lines changed

3 files changed

+169
-5
lines changed

ios/RNRiveError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ func createMalformedFileError() -> NSError {
6464
return NSError(domain: RiveErrorDomain, code: RiveErrorCode.malformedFile.rawValue, userInfo: [NSLocalizedDescriptionKey: "Malformed Rive File", "name": "Malformed"])
6565
}
6666

67+
func createAssetFileError(_ assetName: String) -> NSError {
68+
return NSError(domain: RiveErrorDomain, code: RiveErrorCode.malformedFile.rawValue, userInfo: [NSLocalizedDescriptionKey: "Could not load Rive asset: \(assetName)", "name": "Malformed"])
69+
}
70+
6771
func createIncorrectRiveURL(_ url: String) -> NSError {
6872
return NSError(domain: RiveErrorDomain, code: 900, userInfo: [NSLocalizedDescriptionKey: "Unable to download Rive file from: \(url)", "name": "IncorrectRiveFileURL"])
6973
}

ios/RiveReactNativeView.swift

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
5555
}
5656

5757
@objc var artboardName: String?
58+
59+
@objc var assetsHandled: NSDictionary?
60+
{
61+
didSet {
62+
requiresLocalResourceReconfigure = true;
63+
}
64+
}
5865

5966

6067
@objc var animationName: String?
@@ -79,7 +86,18 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
7986
// MARK: - React Native Helpers
8087

8188
override func removeFromSuperview() {
82-
removeReactSubview(riveView) // TODO: Investigate if this is the optimal place to remove, and if this is necessary.
89+
cleanupResources()
90+
91+
super.removeFromSuperview()
92+
}
93+
94+
private func cleanupResources() {
95+
removeReactSubview(riveView)
96+
riveView?.playerDelegate = nil
97+
riveView?.stateMachineDelegate = nil
98+
riveView = nil;
99+
viewModel?.deregisterView();
100+
viewModel = nil;
83101
}
84102

85103
override func layoutSubviews() {
@@ -90,7 +108,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
90108
}
91109

92110
override func didSetProps(_ changedProps: [String]!) {
93-
if (changedProps.contains("url") || changedProps.contains("resourceName") || changedProps.contains("artboardName") || changedProps.contains("animationName") || changedProps.contains("stateMachineName")) {
111+
if (changedProps.contains("url") || changedProps.contains("resourceName") || changedProps.contains("artboardName") || changedProps.contains("animationName") || changedProps.contains("stateMachineName") || changedProps.contains("assetsHandled")) {
94112
reloadView()
95113
}
96114

@@ -142,11 +160,11 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
142160

143161
let updatedViewModel : RiveViewModel
144162
if let smName = stateMachineName {
145-
updatedViewModel = RiveViewModel(fileName: name, stateMachineName: smName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
163+
updatedViewModel = RiveViewModel(fileName: name, stateMachineName: smName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, customLoader: customLoader)
146164
} else if let animName = animationName {
147-
updatedViewModel = RiveViewModel(fileName: name, animationName: animName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
165+
updatedViewModel = RiveViewModel(fileName: name, animationName: animName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, customLoader: customLoader)
148166
} else {
149-
updatedViewModel = RiveViewModel(fileName: name, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
167+
updatedViewModel = RiveViewModel(fileName: name, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, customLoader: customLoader)
150168
}
151169

152170
updatedViewModel.layoutScaleFactor = layoutScaleFactor.doubleValue
@@ -192,8 +210,149 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
192210
} else {
193211
configureViewModelFromUrl() // TODO: calling viewModel?.configureModel for a URL ViewModel throws. Requires further investigation. Currently recreating the whole ViewModel for certain prop changes.
194212
}
213+
}
214+
215+
private func customLoader(asset: RiveFileAsset, data: Data, factory: RiveFactory) -> Bool {
216+
guard let assetData = assetsHandled?[asset.uniqueName()] as? NSDictionary else {
217+
return false
218+
}
195219

220+
if let source = assetData["source"] as? NSDictionary {
221+
loadAsset(source: source, asset: asset, factory: factory)
222+
return true
223+
}
196224

225+
return false
226+
}
227+
228+
private func loadAsset(source: NSDictionary, asset: RiveFileAsset, factory: RiveFactory) {
229+
let sourceAssetId = source["sourceAssetId"] as? String
230+
let sourceUrl = source["sourceUrl"] as? String
231+
let sourceAsset = source["sourceAsset"] as? String
232+
233+
if let sourceAssetId = sourceAssetId {
234+
handleSourceAssetId(sourceAssetId, asset: asset, factory: factory)
235+
} else if let sourceUrl = sourceUrl {
236+
handleSourceUrl(sourceUrl, asset: asset, factory: factory)
237+
} else if let sourceAsset = sourceAsset {
238+
handleSourceAsset(sourceAsset, path: source["path"] as? String, asset: asset, factory: factory)
239+
}
240+
}
241+
242+
private func handleSourceAssetId(_ sourceAssetId: String, asset: RiveFileAsset, factory: RiveFactory) {
243+
guard URL(string: sourceAssetId) != nil else {
244+
return
245+
}
246+
247+
downloadUrlAsset(url: sourceAssetId) { [weak self] data in
248+
self?.processAssetBytes(data, asset: asset, factory: factory)
249+
}
250+
}
251+
252+
private func handleSourceUrl(_ sourceUrl: String, asset: RiveFileAsset, factory: RiveFactory) {
253+
downloadUrlAsset(url: sourceUrl) { [weak self] data in
254+
self?.processAssetBytes(data, asset: asset, factory: factory)
255+
}
256+
}
257+
258+
private func handleSourceAsset(_ sourceAsset: String, path: String?, asset: RiveFileAsset, factory: RiveFactory) {
259+
loadResourceAsset(sourceAsset: sourceAsset, path: path) {[weak self] data in
260+
self?.processAssetBytes(data, asset: asset, factory: factory)
261+
}
262+
}
263+
264+
private func processAssetBytes(_ data: Data, asset: RiveFileAsset, factory: RiveFactory) {
265+
DispatchQueue.global(qos: .background).async {
266+
switch asset {
267+
case let imageAsset as RiveImageAsset:
268+
let decodedImage = factory.decodeImage(data)
269+
DispatchQueue.main.async {
270+
imageAsset.renderImage(decodedImage)
271+
}
272+
case let fontAsset as RiveFontAsset:
273+
let decodedFont = factory.decodeFont(data)
274+
DispatchQueue.main.async {
275+
fontAsset.font(decodedFont)
276+
}
277+
case let audioAsset as RiveAudioAsset:
278+
let decodedAudio = factory.decodeAudio(data)
279+
DispatchQueue.main.async {
280+
audioAsset.audio(decodedAudio)
281+
}
282+
default:
283+
break
284+
}
285+
}
286+
}
287+
288+
private func downloadUrlAsset(url: String, listener: @escaping (Data) -> Void) {
289+
guard isValidUrl(url) else {
290+
handleInvalidUrlError(url: url)
291+
return
292+
}
293+
294+
let queue = URLSession.shared
295+
guard let requestUrl = URL(string: url) else {
296+
handleInvalidUrlError(url: url)
297+
return
298+
}
299+
300+
let request = URLRequest(url: requestUrl)
301+
let task = queue.dataTask(with: request) {[weak self] data, response, error in
302+
if error != nil {
303+
self?.handleInvalidUrlError(url: url)
304+
} else if let data = data {
305+
listener(data)
306+
}
307+
}
308+
309+
task.resume()
310+
}
311+
312+
private func isValidUrl(_ url: String) -> Bool {
313+
if let url = URL(string: url) {
314+
return UIApplication.shared.canOpenURL(url)
315+
} else {
316+
return false
317+
}
318+
}
319+
320+
private func splitFileNameAndExtension(fileName: String) -> (name: String?, ext: String?)? {
321+
let components = fileName.split(separator: ".")
322+
guard components.count == 2 else { return nil }
323+
return (name: String(components[0]), ext: String(components[1]))
324+
}
325+
326+
private func loadResourceAsset(sourceAsset: String, path: String?, listener: @escaping (Data) -> Void) {
327+
328+
guard let splitSourceAssetName = splitFileNameAndExtension(fileName: sourceAsset),
329+
let name = splitSourceAssetName.name,
330+
let ext = splitSourceAssetName.ext else {
331+
handleRiveError(error: createAssetFileError(sourceAsset))
332+
return
333+
}
334+
335+
guard let folderUrl = Bundle.main.url(forResource: name, withExtension: ext) else {
336+
handleRiveError(error: createAssetFileError(sourceAsset))
337+
return
338+
}
339+
340+
DispatchQueue.global(qos: .background).async { [weak self] in
341+
do {
342+
let fileData = try Data(contentsOf: folderUrl)
343+
DispatchQueue.main.async {
344+
listener(fileData)
345+
}
346+
} catch {
347+
DispatchQueue.main.async {
348+
self?.handleRiveError(error: createAssetFileError(sourceAsset))
349+
}
350+
}
351+
}
352+
}
353+
354+
private func handleInvalidUrlError(url: String) {
355+
handleRiveError(error: createIncorrectRiveURL(url))
197356
}
198357

199358
// MARK: - Playback Controls

ios/RiveReactNativeViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ @interface RCT_EXTERN_MODULE(RiveReactNativeViewManager, RCTViewManager)
1010
RCT_EXPORT_VIEW_PROPERTY(alignment, NSString)
1111
RCT_EXPORT_VIEW_PROPERTY(autoplay, BOOL)
1212
RCT_EXPORT_VIEW_PROPERTY(artboardName, NSString)
13+
RCT_EXPORT_VIEW_PROPERTY(assetsHandled, NSDictionary)
1314
RCT_EXPORT_VIEW_PROPERTY(animationName, NSString)
1415
RCT_EXPORT_VIEW_PROPERTY(stateMachineName, NSString)
1516
RCT_EXPORT_VIEW_PROPERTY(isUserHandlingErrors, BOOL)

0 commit comments

Comments
 (0)