Skip to content

Commit

Permalink
feat: add image OOB asset API for iOS native wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
zplata committed Mar 17, 2024
1 parent 4eef109 commit 6293de3
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 13 deletions.
Binary file added example/ios/Assets/cat-994454.webp
Binary file not shown.
Binary file added example/ios/Assets/cat_wall.riv
Binary file not shown.
12 changes: 6 additions & 6 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -487,10 +487,10 @@ PODS:
- React-jsi (= 0.72.7)
- React-logger (= 0.72.7)
- React-perflogger (= 0.72.7)
- rive-react-native (6.2.2):
- rive-react-native (6.2.3):
- React-Core
- RiveRuntime (= 5.5.1)
- RiveRuntime (5.5.1)
- RiveRuntime (= 5.9.1)
- RiveRuntime (5.9.1)
- RNCMaskedView (0.2.9):
- React-Core
- RNCPicker (1.16.8):
Expand Down Expand Up @@ -744,8 +744,8 @@ SPEC CHECKSUMS:
React-runtimescheduler: 7649c3b46c8dee1853691ecf60146a16ae59253c
React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415
ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6
rive-react-native: 201f67cb0ef9d59958cfffc078ec386d9854a8ac
RiveRuntime: b57830ff73f406f3b4022f457b16690535ca4d05
rive-react-native: 057e8f03efddf92ee16c0ed8978b7ac7328d9a4b
RiveRuntime: 29c140c4f91a8027e7577ffb99f722f0a8029b5f
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNCPicker: 0991c56da7815c0cf946d6f63cf920b25296e5f6
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
Expand All @@ -757,4 +757,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 2e1de35b032a6e591da83a5975df1059c489c1d8

COCOAPODS: 1.11.3
COCOAPODS: 1.14.3
20 changes: 18 additions & 2 deletions example/ios/RiveReactNativeExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
9D879D0D265BF2A400D01424 /* ui_swipe_left_to_delete.riv in Resources */ = {isa = PBXBuildFile; fileRef = 9D879D0C265BF2A400D01424 /* ui_swipe_left_to_delete.riv */; };
9DBF1CC52684937E0008391A /* v6_file.riv in Resources */ = {isa = PBXBuildFile; fileRef = 9DBF1CC42684937E0008391A /* v6_file.riv */; };
C3C07472283BE07300E8EB33 /* hero_editor.riv in Resources */ = {isa = PBXBuildFile; fileRef = C3C07471283BE07300E8EB33 /* hero_editor.riv */; };
E5119C452B59DD4A0051E08A /* cat-994454.webp in Resources */ = {isa = PBXBuildFile; fileRef = E5119C442B59DD4A0051E08A /* cat-994454.webp */; };
E554409B2A79DC8100D550DE /* hello_world_text.riv in Resources */ = {isa = PBXBuildFile; fileRef = E554409A2A79DC8100D550DE /* hello_world_text.riv */; };
E5637D7A292BD27F000CBC1E /* skills_listener.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5637D79292BD26D000CBC1E /* skills_listener.riv */; };
E59C19102B07F6FE002F0CBA /* nested_menu.riv in Resources */ = {isa = PBXBuildFile; fileRef = E59C190F2B07F6FE002F0CBA /* nested_menu.riv */; };
E5A17A90299AA0F5008CC433 /* avatars.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5A17A8F299AA0F5008CC433 /* avatars.riv */; };
E5E405172B53560900751A94 /* cat_wall.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5E405162B53560900751A94 /* cat_wall.riv */; };
E5F3FC4029B2661500D6D265 /* switch.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5F3FC3F29B2661500D6D265 /* switch.riv */; };
E5FC4EAA2ABB975100D98158 /* rating.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5FC4EA92ABB975100D98158 /* rating.riv */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -93,10 +95,12 @@
CC5818FED9124A6CA1145A8C /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = "<group>"; };
D9C4F25984EC4D5086341FDB /* Zocial.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Zocial.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Zocial.ttf"; sourceTree = "<group>"; };
E00ACF0FDA8BF921659E2F9A /* Pods-RiveReactNativeExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiveReactNativeExample.release.xcconfig"; path = "Target Support Files/Pods-RiveReactNativeExample/Pods-RiveReactNativeExample.release.xcconfig"; sourceTree = "<group>"; };
E5119C442B59DD4A0051E08A /* cat-994454.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = "cat-994454.webp"; sourceTree = "<group>"; };
E554409A2A79DC8100D550DE /* hello_world_text.riv */ = {isa = PBXFileReference; lastKnownFileType = file; name = hello_world_text.riv; path = ../../android/app/src/main/res/raw/hello_world_text.riv; sourceTree = "<group>"; };
E5637D79292BD26D000CBC1E /* skills_listener.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = skills_listener.riv; sourceTree = "<group>"; };
E59C190F2B07F6FE002F0CBA /* nested_menu.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = nested_menu.riv; sourceTree = "<group>"; };
E5A17A8F299AA0F5008CC433 /* avatars.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = avatars.riv; sourceTree = "<group>"; };
E5E405162B53560900751A94 /* cat_wall.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = cat_wall.riv; sourceTree = "<group>"; };
E5F3FC3F29B2661500D6D265 /* switch.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = switch.riv; sourceTree = "<group>"; };
E5FC4EA92ABB975100D98158 /* rating.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = rating.riv; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -252,10 +256,12 @@
042FD22626B81BD1004556A3 /* constrained.riv */,
04A886F226A990050078530A /* two_bone_ik.riv */,
9DBF1CC42684937E0008391A /* v6_file.riv */,
E5E405162B53560900751A94 /* cat_wall.riv */,
E5F3FC3F29B2661500D6D265 /* switch.riv */,
9D879D0C265BF2A400D01424 /* ui_swipe_left_to_delete.riv */,
E5FC4EA92ABB975100D98158 /* rating.riv */,
E5A17A8F299AA0F5008CC433 /* avatars.riv */,
E5119C442B59DD4A0051E08A /* cat-994454.webp */,
E59C190F2B07F6FE002F0CBA /* nested_menu.riv */,
E5637D79292BD26D000CBC1E /* skills_listener.riv */,
9D879D0A26578A5E00D01424 /* artboard_animations.riv */,
Expand Down Expand Up @@ -405,6 +411,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E5119C452B59DD4A0051E08A /* cat-994454.webp in Resources */,
9D879D00265642BA00D01424 /* truck_v7.riv in Resources */,
E5FC4EAA2ABB975100D98158 /* rating.riv in Resources */,
042FD22726B81BD1004556A3 /* constrained.riv in Resources */,
Expand All @@ -413,6 +420,7 @@
04A886F326A990050078530A /* two_bone_ik.riv in Resources */,
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
9DBF1CC52684937E0008391A /* v6_file.riv in Resources */,
E5E405172B53560900751A94 /* cat_wall.riv in Resources */,
9D4FE6122649427F0098BF6A /* bird.riv in Resources */,
E554409B2A79DC8100D550DE /* hello_world_text.riv in Resources */,
9D879D0D265BF2A400D01424 /* ui_swipe_left_to_delete.riv in Resources */,
Expand Down Expand Up @@ -907,7 +915,11 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
};
Expand Down Expand Up @@ -964,7 +976,11 @@
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
Expand Down
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import MeshExample from './MeshExample';
import DynamicText from './DynamicText';
import NestedInputs from './NestedInputs';
import Events from './Events';
import OutOfBandAssets from './OutOfBandAssets';

const Stack = createStackNavigator();

Expand All @@ -38,6 +39,7 @@ export default function App() {
<Stack.Screen name="Events" component={Events} />
<Stack.Screen name="NestedInputs" component={NestedInputs} />
<Stack.Screen name="DynamicText" component={DynamicText} />
<Stack.Screen name="OutOfBandAssets" component={OutOfBandAssets} />
<Stack.Screen
name="MultipleArtboards"
component={MultipleArtboards}
Expand Down
8 changes: 8 additions & 0 deletions example/src/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export default function Home({ navigation }) {
Dynamic Text
</Button>

<Button
mode="contained"
onPress={() => navigation.navigate('OutOfBandAssets')}
style={styles.buttonStyle}
>
Out of Band Assets
</Button>

<Button
mode="contained"
onPress={() => navigation.navigate('ErrorNotHandled')}
Expand Down
68 changes: 68 additions & 0 deletions example/src/OutOfBandAssets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import { SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native';
import Rive, { Fit } from 'rive-react-native';

export default function StateMachine() {
return (
<SafeAreaView style={styles.safeAreaViewContainer}>
<ScrollView contentContainerStyle={styles.container}>
<Rive
autoplay={true}
fit={Fit.Cover}
style={styles.box}
stateMachineName="State Machine 2"
artboardName="Picture 1"
initialAssetsHandled={{
'cat.webp': {
willHandleAsset: true,
// assetUrl: 'https://www.gstatic.com/webp/gallery/1.webp',
bundledAssetName: 'cat-994454.webp',
},
}}
resourceName={'cat_wall'}
/>
<Text>
Load in an external asset from a URL or bundled asset on the native
platform
</Text>
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
safeAreaViewContainer: {
flex: 1,
},
container: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 150,
padding: 10,
},
wrapper: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
marginBottom: 20,
},
box: {
width: '100%',
height: 500,
marginVertical: 20,
},
picker: {
width: '100%',
height: 50,
},
radioButtonsWrapper: {
flexDirection: 'row',
},
radioButtonWrapper: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 16,
},
});
Binary file added example/src/cat-994454.webp
Binary file not shown.
131 changes: 129 additions & 2 deletions ios/RiveReactNativeView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import UIKit
import RiveRuntime

struct FileHandlerOptions {
var willHandleAsset: Bool
var bundledAssetName: String?
var assetUrl: String?

init(from dictionary: NSDictionary) {
// If the user omits this property but supplies a bundledAssetName, we can
// most likely assume they meant to handle the asset themselves since they
// want to load the specified asset from the bundle
self.willHandleAsset = dictionary["willHandleAsset"] as? Bool ?? (dictionary["bundledAssetName"] as? String)?.isEmpty ?? false
self.bundledAssetName = dictionary["bundledAssetName"] as? String ?? nil
self.assetUrl = dictionary["assetUrl"] as? String ?? nil
}
}

struct SwiftFilesHandled {
var files: [String: FileHandlerOptions]

init(from rnFilesHandled: NSDictionary) {
var filesHandled = [String: FileHandlerOptions]()
if let dictionary = rnFilesHandled as? [String: NSDictionary] {
for (key, value) in dictionary {
filesHandled[key] = FileHandlerOptions(from: value)
}
}
self.files = filesHandled
}
}

class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate {
// MARK: RiveReactNativeView Properties
private var resourceFromBundle = true
Expand All @@ -14,6 +43,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
@objc var onStateChanged: RCTDirectEventBlock?
@objc var onRiveEventReceived: RCTDirectEventBlock?
@objc var onError: RCTDirectEventBlock?
@objc var onCustomAssetLoader: RCTDirectEventBlock?
@objc var isUserHandlingErrors: Bool

// MARK: RiveRuntime Bindings
Expand All @@ -39,6 +69,8 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
}
}

@objc var initialAssetsHandled: NSDictionary?

@objc var fit: String?

@objc var alignment: String?
Expand Down Expand Up @@ -129,14 +161,68 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
riveView?.stateMachineDelegate = self
}

private func isImageFile(fileName: String) -> Bool {
// May need a better way to detect image assets
let imageFileExtensions = ["jpg", "jpeg", "png", "webp"]

let lowercasedFileName = fileName.lowercased()
for ext in imageFileExtensions {
if lowercasedFileName.hasSuffix(".\(ext)") {
return true
}
}
return false
}

private func splitFileNameAndExtension(fileName: String) -> (name: String, ext: String)? {
let components = fileName.components(separatedBy: ".")

// Ensure there is at least one period in the filename
guard components.count > 1 else {
return nil
}

// Extract the name and extension
let name = components.dropLast().joined(separator: ".")
let ext = components.last!

return (name, ext)
}

private func configureViewModelFromResource() {
if let name = resourceName {
url = nil
resourceFromBundle = true

let updatedViewModel : RiveViewModel
if let smName = stateMachineName {
updatedViewModel = RiveViewModel(fileName: name, stateMachineName: smName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
updatedViewModel = RiveViewModel(fileName: name, stateMachineName: smName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, loadCdn: true,
customLoader: {(asset: RiveFileAsset, data: Data, factory: RiveFactory) -> Bool in

// Check for configuration that dictates whether to load in assets from the bundle here
// and/or whether the asset loading should be handled by the consumer
//
// Note: This config object is used to avoid synchronous calls over the bridge to React Native
// as this is not recommended. So we use a config object for consumers to dictate what assets are
// included in the xcode project that we can load in, and the return value for customAssetLoader
if let workingFilesHandled = self.initialAssetsHandled {
let convertedFilesHandled = SwiftFilesHandled(from: workingFilesHandled)
if let fileToHandle = convertedFilesHandled.files["\(asset.name())"] {
// Load asset flow:
// 1. Load from bundle if bundle asset name provided
// 2. Load from URL if url to load from is provided
// 3. Load the raw bytes passed in from RN if provided
if let bundleAssetName = fileToHandle.bundledAssetName as String? {
self.handleBundleAsset(bundleAssetName: bundleAssetName, asset: asset, factory: factory)
} else if let assetUrl = fileToHandle.assetUrl as String? {
self.handleUrlAsset(assetUrl: assetUrl, asset: asset, factory: factory)
}
return fileToHandle.willHandleAsset;
}
}
return false
}
)
} else if let animName = animationName {
updatedViewModel = RiveViewModel(fileName: name, animationName: animName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
} else {
Expand All @@ -148,14 +234,55 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
}
}

private func handleBundleAsset(bundleAssetName: String, asset: RiveFileAsset, factory: RiveFactory) {
let splitBundleAssetName = self.splitFileNameAndExtension(fileName: bundleAssetName)
guard let folderUrl = Bundle.main.url(forResource: splitBundleAssetName?.name, withExtension: splitBundleAssetName?.ext) else {
fatalError("Could not find the asset \(bundleAssetName)")
}
// TODO: also handle fonts
do {
let fileData = try Data(contentsOf: folderUrl)
if self.isImageFile(fileName: bundleAssetName) {
let renderImage = factory.decodeImage(fileData)
(asset as! RiveImageAsset).renderImage(renderImage)
}
} catch {
fatalError("Could not create a Rive render image for \(bundleAssetName)")
}
}

private func handleUrlAsset(assetUrl: String, asset: RiveFileAsset, factory: RiveFactory) {
let url = URL(string: assetUrl)!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
if self.isImageFile(fileName: asset.name()) {
let renderImage = factory.decodeImage(data)
debugPrint("IS IMAGE \(renderImage)");
(asset as! RiveImageAsset).renderImage(renderImage)
}
}

// if (first){
// first=false;
// if let fontAsset = self.cachedFont, let font=self.fontCache.randomElement() {
// fontAsset.font(font);
// }
// }

}
task.resume()
}

private func configureViewModelFromUrl() {
if let url = url {
resourceName = nil
resourceFromBundle = false

let updatedViewModel : RiveViewModel
// TODO: Need to make change in rive-ios to add customLoader callback when passing a URL for the Rive asset
if let smName = stateMachineName {
updatedViewModel = RiveViewModel(webURL: url, stateMachineName: smName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
updatedViewModel = RiveViewModel(webURL: url, stateMachineName: smName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, loadCdn: true, artboardName: artboardName
)
} else if let animName = animationName {
updatedViewModel = RiveViewModel(webURL: url, animationName: animName, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName)
} else {
Expand Down
Loading

1 comment on commit 6293de3

@evelant
Copy link

@evelant evelant commented on 6293de3 Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any indication of when this branch might get completed and merged? We really need OOB assets in react-native!

Please sign in to comment.