diff --git a/android/src/main/java/com/rivereactnative/RNRiveError.kt b/android/src/main/java/com/rivereactnative/RNRiveError.kt index d2e80b01..05263b05 100644 --- a/android/src/main/java/com/rivereactnative/RNRiveError.kt +++ b/android/src/main/java/com/rivereactnative/RNRiveError.kt @@ -25,43 +25,43 @@ enum class RNRiveError(private val mValue: String) { return when (ex) { is ArtboardException -> { val err = IncorrectArtboardName - err.message = ex.message!! + err.message = ex.message ?: "Unknown artboard exception" return err } is UnsupportedRuntimeVersionException -> { val err = UnsupportedRuntimeVersion - err.message = ex.message!! + err.message = ex.message ?: "Unsupported runtime version" return err } is MalformedFileException -> { val err = MalformedFile - err.message = ex.message!! + err.message = ex.message ?: "Malformed file" return err } is AnimationException -> { val err = IncorrectAnimationName - err.message = ex.message!! + err.message = ex.message ?: "Unknown animation exception" return err } is StateMachineException -> { val err = IncorrectStateMachineName - err.message = ex.message!! + err.message = ex.message ?: "Unknown state machine exception" return err } is StateMachineInputException -> { val err = IncorrectStateMachineInput - err.message = ex.message!! + err.message = ex.message ?: "Unknown state machine input exception" return err } is TextValueRunException -> { val err = TextRunNotFoundError - err.message = ex.message!! + err.message = ex.message ?: "Text run not found" return err } is ViewModelException -> { val err = DataBindingError - err.message = ex.message!! - return err; + err.message = ex.message ?: "Data binding error" + return err } else -> null } diff --git a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt index 25746a83..8a128902 100644 --- a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt +++ b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.res.Resources import android.graphics.Color import android.net.Uri +import android.util.Log import android.widget.FrameLayout import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -82,6 +83,7 @@ class ReactNativeRiveAnimationView(private val context: ThemedReactContext) : @SuppressLint("ViewConstructor") class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout(context) { + private val TAG = "RiveReactNativeView" private var riveAnimationView: ReactNativeRiveAnimationView? = null private var resourceName: String? = null private var resId: Int = -1 @@ -124,7 +126,7 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout if (animation is LinearAnimationInstance) { onLoopEnd(animation.name, RNLoopMode.mapToRNLoopMode(animation.loop)) } else { - throw IllegalArgumentException("Only animation can be passed as an argument") + Log.e(TAG, "notifyLoop: Expected LinearAnimationInstance but got ${animation.javaClass.simpleName}") } } @@ -322,15 +324,18 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout fun pause() { try { - if (riveAnimationView?.playingAnimations?.isNotEmpty() == true) { - riveAnimationView!!.pause(riveAnimationView!!.playingAnimations.first().name) - } else if (riveAnimationView?.playingStateMachines?.isNotEmpty() == true) { - riveAnimationView!!.pause(riveAnimationView!!.playingStateMachines.first().name, true) + val view = riveAnimationView ?: return + if (view.playingAnimations.isNotEmpty()) { + view.pause(view.playingAnimations.first().name) + } else if (view.playingStateMachines.isNotEmpty()) { + view.pause(view.playingStateMachines.first().name, true) } else { - riveAnimationView?.pause() + view.pause() } } catch (ex: RiveException) { handleRiveException(ex) + } catch (ex: Exception) { + Log.e(TAG, "Error in pause: ${ex.message}") } } @@ -355,11 +360,19 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } fun touchBegan(x: Float, y: Float) { - riveAnimationView?.controller?.pointerEvent(PointerEvents.POINTER_DOWN, x, y) + try { + riveAnimationView?.controller?.pointerEvent(PointerEvents.POINTER_DOWN, x, y) + } catch (ex: Exception) { + Log.e(TAG, "Error in touchBegan: ${ex.message}") + } } fun touchEnded(x: Float, y: Float) { - riveAnimationView?.controller?.pointerEvent(PointerEvents.POINTER_UP, x, y) + try { + riveAnimationView?.controller?.pointerEvent(PointerEvents.POINTER_UP, x, y) + } catch (ex: Exception) { + Log.e(TAG, "Error in touchEnded: ${ex.message}") + } } fun setTextRunValue(textRunName: String, textValue: String) { @@ -498,16 +511,23 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout val viewModel = file.defaultViewModelForArtboard(artboard) fun bindInstance(instance: ViewModelInstance) { - riveAnimationView?.controller?.stateMachines?.first()?.viewModelInstance = instance - riveAnimationView?.controller?.activeArtboard?.viewModelInstance = instance - - // Re-register the listener if the listener wasn't added on this view model instance. - // As calling registerPropertyListener from JS may have been done before/after/during - // this configuration. - propertyListeners.toList().forEach { (_, listener) -> - if (listener.instance != instance) { - registerPropertyListener(listener.path, listener.propertyType) + try { + val stateMachines = riveAnimationView?.controller?.stateMachines + if (stateMachines != null && stateMachines.isNotEmpty()) { + stateMachines.first().viewModelInstance = instance + } + riveAnimationView?.controller?.activeArtboard?.viewModelInstance = instance + + // Re-register the listener if the listener wasn't added on this view model instance. + // As calling registerPropertyListener from JS may have been done before/after/during + // this configuration. + propertyListeners.toList().forEach { (_, listener) -> + if (listener.instance != instance) { + registerPropertyListener(listener.path, listener.propertyType) + } } + } catch (ex: Exception) { + Log.e(TAG, "Error in bindInstance: ${ex.message}") } } @@ -848,8 +868,13 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout fun getBooleanState(inputName: String): Boolean? { return try { - val smi = riveAnimationView?.controller?.stateMachines?.get(0) - val smiInput = smi?.input(inputName) + val stateMachines = riveAnimationView?.controller?.stateMachines + if (stateMachines == null || stateMachines.isEmpty()) { + Log.e(TAG, "getBooleanState: No state machines available") + return null + } + val smi = stateMachines[0] + val smiInput = smi.input(inputName) if (smiInput is SMIBoolean) { smiInput.value } else { @@ -858,6 +883,9 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } catch (ex: RiveException) { handleRiveException(ex) null + } catch (ex: Exception) { + Log.e(TAG, "Error in getBooleanState: ${ex.message}") + null } } @@ -871,8 +899,13 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout fun getNumberState(inputName: String): Float? { return try { - val smi = riveAnimationView?.controller?.stateMachines?.get(0) - val smiInput = smi?.input(inputName) + val stateMachines = riveAnimationView?.controller?.stateMachines + if (stateMachines == null || stateMachines.isEmpty()) { + Log.e(TAG, "getNumberState: No state machines available") + return null + } + val smi = stateMachines[0] + val smiInput = smi.input(inputName) if (smiInput is SMINumber) { smiInput.value } else { @@ -881,6 +914,9 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } catch (ex: RiveException) { handleRiveException(ex) null + } catch (ex: Exception) { + Log.e(TAG, "Error in getNumberState: ${ex.message}") + null } } @@ -902,8 +938,11 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout fun getBooleanStateAtPath(inputName: String, path: String): Boolean? { return try { - val artboard = riveAnimationView?.controller?.activeArtboard - val smiInput = artboard?.input(inputName, path) + val artboard = riveAnimationView?.controller?.activeArtboard ?: run { + Log.e(TAG, "getBooleanStateAtPath: No active artboard available") + return null + } + val smiInput = artboard.input(inputName, path) if (smiInput is SMIBoolean) { smiInput.value } else { @@ -912,6 +951,9 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } catch (ex: RiveException) { handleRiveException(ex) null + } catch (ex: Exception) { + Log.e(TAG, "Error in getBooleanStateAtPath: ${ex.message}") + null } } @@ -925,8 +967,11 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout fun getNumberStateAtPath(inputName: String, path: String): Float? { return try { - val artboard = riveAnimationView?.controller?.activeArtboard - val smiInput = artboard?.input(inputName, path) + val artboard = riveAnimationView?.controller?.activeArtboard ?: run { + Log.e(TAG, "getNumberStateAtPath: No active artboard available") + return null + } + val smiInput = artboard.input(inputName, path) if (smiInput is SMINumber) { smiInput.value } else { @@ -935,6 +980,9 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } catch (ex: RiveException) { handleRiveException(ex) null + } catch (ex: Exception) { + Log.e(TAG, "Error in getNumberStateAtPath: ${ex.message}") + null } } @@ -950,12 +998,14 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout } private fun handleFileNotFound() { + val errorMessage = "File resource not found. You must provide correct url or resourceName!" if (isUserHandlingErrors) { val rnRiveError = RNRiveError.FileNotFound - rnRiveError.message = "File resource not found. You must provide correct url or resourceName!" + rnRiveError.message = errorMessage sendErrorToRN(rnRiveError) } else { - throw IllegalStateException("File resource not found. You must provide correct url or resourceName!") + Log.e(TAG, errorMessage) + showRNRiveError(errorMessage, null) } } diff --git a/android/src/main/java/com/rivereactnative/RiveReactNativeViewManager.kt b/android/src/main/java/com/rivereactnative/RiveReactNativeViewManager.kt index f609bb71..5f9c2a6e 100644 --- a/android/src/main/java/com/rivereactnative/RiveReactNativeViewManager.kt +++ b/android/src/main/java/com/rivereactnative/RiveReactNativeViewManager.kt @@ -1,5 +1,6 @@ package com.rivereactnative +import android.util.Log import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.MapBuilder @@ -8,6 +9,7 @@ import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp class RiveReactNativeViewManager : SimpleViewManager() { + private val TAG = "RiveReactNativeViewManager" override fun getExportedCustomDirectEventTypeConstants(): MutableMap>? { val builder: MapBuilder.Builder> = MapBuilder.builder>() @@ -25,15 +27,18 @@ class RiveReactNativeViewManager : SimpleViewManager() { "play" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val animationName = it.getString(0)!! - val loopMode = it.getString(1)!! - val direction = it.getString(2)!! - val isStateMachine = it.getBoolean(3)!! - view.run { - val rnLoopMode = RNLoopMode.mapToRNLoopMode(loopMode) - val rnDirection = RNDirection.mapToRNDirection(direction) - play(animationName, rnLoopMode, rnDirection, isStateMachine) + try { + val animationName = it.getString(0) ?: "" + val loopMode = it.getString(1) ?: "loop" + val direction = it.getString(2) ?: "forwards" + val isStateMachine = it.getBoolean(3) + view.run { + val rnLoopMode = RNLoopMode.mapToRNLoopMode(loopMode) + val rnDirection = RNDirection.mapToRNDirection(direction) + play(animationName, rnLoopMode, rnDirection, isStateMachine) + } + } catch (e: Exception) { + Log.e(TAG, "Error in play command: ${e.message}") } } @@ -47,125 +52,216 @@ class RiveReactNativeViewManager : SimpleViewManager() { "fireState" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val stateMachineName = it.getString(0)!! - val inputName = it.getString(1)!! - view.fireState(stateMachineName, inputName) + try { + val stateMachineName = it.getString(0) ?: "" + val inputName = it.getString(1) ?: "" + if (stateMachineName.isEmpty() || inputName.isEmpty()) { + Log.e(TAG, "fireState: stateMachineName or inputName is empty") + return@let + } + view.fireState(stateMachineName, inputName) + } catch (e: Exception) { + Log.e(TAG, "Error in fireState command: ${e.message}") + } } } "setBooleanState" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val stateMachineName = it.getString(0)!! - val inputName = it.getString(1)!! - val value = it.getBoolean(2)!! - view.setBooleanState(stateMachineName, inputName, value) + try { + val stateMachineName = it.getString(0) ?: "" + val inputName = it.getString(1) ?: "" + if (stateMachineName.isEmpty() || inputName.isEmpty()) { + Log.e(TAG, "setBooleanState: stateMachineName or inputName is empty") + return@let + } + val value = it.getBoolean(2) + view.setBooleanState(stateMachineName, inputName, value) + } catch (e: Exception) { + Log.e(TAG, "Error in setBooleanState command: ${e.message}") + } } } "setNumberState" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val stateMachineName = it.getString(0)!! - val inputName = it.getString(1)!! - val value = it.getDouble(2)!! - view.setNumberState(stateMachineName, inputName, value.toFloat()) + try { + val stateMachineName = it.getString(0) ?: "" + val inputName = it.getString(1) ?: "" + if (stateMachineName.isEmpty() || inputName.isEmpty()) { + Log.e(TAG, "setNumberState: stateMachineName or inputName is empty") + return@let + } + val value = it.getDouble(2) + view.setNumberState(stateMachineName, inputName, value.toFloat()) + } catch (e: Exception) { + Log.e(TAG, "Error in setNumberState command: ${e.message}") + } } } "fireStateAtPath" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val inputName = it.getString(0)!! - val path = it.getString(1)!! - view.fireStateAtPath(inputName, path) + try { + val inputName = it.getString(0) ?: "" + val path = it.getString(1) ?: "" + if (inputName.isEmpty() || path.isEmpty()) { + Log.e(TAG, "fireStateAtPath: inputName or path is empty") + return@let + } + view.fireStateAtPath(inputName, path) + } catch (e: Exception) { + Log.e(TAG, "Error in fireStateAtPath command: ${e.message}") + } } } "setBooleanStateAtPath" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val inputName = it.getString(0)!! - val value = it.getBoolean(1)!! - val path = it.getString(2)!! - view.setBooleanStateAtPath(inputName, value, path) + try { + val inputName = it.getString(0) ?: "" + val path = it.getString(2) ?: "" + if (inputName.isEmpty() || path.isEmpty()) { + Log.e(TAG, "setBooleanStateAtPath: inputName or path is empty") + return@let + } + val value = it.getBoolean(1) + view.setBooleanStateAtPath(inputName, value, path) + } catch (e: Exception) { + Log.e(TAG, "Error in setBooleanStateAtPath command: ${e.message}") + } } } "setNumberStateAtPath" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val inputName = it.getString(0)!! - val value = it.getDouble(1)!! - val path = it.getString(2)!! - view.setNumberStateAtPath(inputName, value.toFloat(), path) + try { + val inputName = it.getString(0) ?: "" + val path = it.getString(2) ?: "" + if (inputName.isEmpty() || path.isEmpty()) { + Log.e(TAG, "setNumberStateAtPath: inputName or path is empty") + return@let + } + val value = it.getDouble(1) + view.setNumberStateAtPath(inputName, value.toFloat(), path) + } catch (e: Exception) { + Log.e(TAG, "Error in setNumberStateAtPath command: ${e.message}") + } } } // Data Binding "setBooleanPropertyValue" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - val value = it.getBoolean(1)!! - view.setBooleanPropertyValue(path, value) + try { + val path = it.getString(0) ?: "" + if (path.isEmpty()) { + Log.e(TAG, "setBooleanPropertyValue: path is empty") + return@let + } + val value = it.getBoolean(1) + view.setBooleanPropertyValue(path, value) + } catch (e: Exception) { + Log.e(TAG, "Error in setBooleanPropertyValue command: ${e.message}") + } } } "setStringPropertyValue" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - val value = it.getString(1)!! - view.setStringPropertyValue(path, value) + try { + val path = it.getString(0) ?: "" + val value = it.getString(1) ?: "" + if (path.isEmpty()) { + Log.e(TAG, "setStringPropertyValue: path is empty") + return@let + } + view.setStringPropertyValue(path, value) + } catch (e: Exception) { + Log.e(TAG, "Error in setStringPropertyValue command: ${e.message}") + } } } "setNumberPropertyValue" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - val value = it.getDouble(1)!! - view.setNumberPropertyValue(path, value.toFloat()) + try { + val path = it.getString(0) ?: "" + if (path.isEmpty()) { + Log.e(TAG, "setNumberPropertyValue: path is empty") + return@let + } + val value = it.getDouble(1) + view.setNumberPropertyValue(path, value.toFloat()) + } catch (e: Exception) { + Log.e(TAG, "Error in setNumberPropertyValue command: ${e.message}") + } } } "setColorPropertyValue" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - val r = it.getDouble(1)!! - val g = it.getDouble(2)!! - val b = it.getDouble(3)!! - val a = it.getDouble(4)!! - view.setColorPropertyValue(path, r.toInt(), g.toInt(), b.toInt(), a.toInt()) + try { + val path = it.getString(0) ?: "" + if (path.isEmpty()) { + Log.e(TAG, "setColorPropertyValue: path is empty") + return@let + } + val r = it.getDouble(1) + val g = it.getDouble(2) + val b = it.getDouble(3) + val a = it.getDouble(4) + view.setColorPropertyValue(path, r.toInt(), g.toInt(), b.toInt(), a.toInt()) + } catch (e: Exception) { + Log.e(TAG, "Error in setColorPropertyValue command: ${e.message}") + } } } "setEnumPropertyValue" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - val value = it.getString(1)!! - view.setEnumPropertyValue(path, value) + try { + val path = it.getString(0) ?: "" + val value = it.getString(1) ?: "" + if (path.isEmpty()) { + Log.e(TAG, "setEnumPropertyValue: path is empty") + return@let + } + view.setEnumPropertyValue(path, value) + } catch (e: Exception) { + Log.e(TAG, "Error in setEnumPropertyValue command: ${e.message}") + } } } "fireTriggerProperty" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - view.fireTriggerProperty(path)!! + try { + val path = it.getString(0) ?: "" + if (path.isEmpty()) { + Log.e(TAG, "fireTriggerProperty: path is empty") + return@let + } + view.fireTriggerProperty(path) // Remove the !! as this method returns Unit (void) + } catch (e: Exception) { + Log.e(TAG, "Error in fireTriggerProperty command: ${e.message}") + } } } "registerPropertyListener" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val path = it.getString(0)!! - val propertyType = it.getString(1)!! - view.registerPropertyListener(path, propertyType) + try { + val path = it.getString(0) ?: "" + val propertyType = it.getString(1) ?: "" + if (path.isEmpty() || propertyType.isEmpty()) { + Log.e(TAG, "registerPropertyListener: path or propertyType is empty") + return@let + } + view.registerPropertyListener(path, propertyType) + } catch (e: Exception) { + Log.e(TAG, "Error in registerPropertyListener command: ${e.message}") + } } } @@ -173,19 +269,25 @@ class RiveReactNativeViewManager : SimpleViewManager() { "touchBegan" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val x: Double = it.getDouble(0)!! - val y: Double = it.getDouble(1)!! - view.touchBegan(x.toFloat(), y.toFloat()) + try { + val x = it.getDouble(0) + val y = it.getDouble(1) + view.touchBegan(x.toFloat(), y.toFloat()) + } catch (e: Exception) { + Log.e(TAG, "Error in touchBegan command: ${e.message}") + } } } "touchEnded" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val x: Double = it.getDouble(0)!! - val y: Double = it.getDouble(1)!! - view.touchEnded(x.toFloat(), y.toFloat()) + try { + val x = it.getDouble(0) + val y = it.getDouble(1) + view.touchEnded(x.toFloat(), y.toFloat()) + } catch (e: Exception) { + Log.e(TAG, "Error in touchEnded command: ${e.message}") + } } } @@ -193,20 +295,34 @@ class RiveReactNativeViewManager : SimpleViewManager() { "setTextRunValue" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val textRunName: String = it.getString(0)!! - val textValue: String = it.getString(1)!! - view.setTextRunValue(textRunName, textValue) + try { + val textRunName = it.getString(0) ?: "" + val textValue = it.getString(1) ?: "" + if (textRunName.isEmpty()) { + Log.e(TAG, "setTextRunValue: textRunName is empty") + return@let + } + view.setTextRunValue(textRunName, textValue) + } catch (e: Exception) { + Log.e(TAG, "Error in setTextRunValue command: ${e.message}") + } } } "setTextRunValueAtPath" -> { args?.let { - // Don't remove the !! - some versions of Android/Kotlin/Android-Studio may return null - val textRunName: String = it.getString(0)!! - val textValue: String = it.getString(1)!! - val path: String = it.getString(2)!! - view.setTextRunValueAtPath(textRunName, textValue, path) + try { + val textRunName = it.getString(0) ?: "" + val textValue = it.getString(1) ?: "" + val path = it.getString(2) ?: "" + if (textRunName.isEmpty() || path.isEmpty()) { + Log.e(TAG, "setTextRunValueAtPath: textRunName or path is empty") + return@let + } + view.setTextRunValueAtPath(textRunName, textValue, path) + } catch (e: Exception) { + Log.e(TAG, "Error in setTextRunValueAtPath command: ${e.message}") + } } } diff --git a/ios/RNAlignment.swift b/ios/RNAlignment.swift index 357f8a1f..8164e484 100644 --- a/ios/RNAlignment.swift +++ b/ios/RNAlignment.swift @@ -11,15 +11,17 @@ enum RNAlignment: String { case BottomLeft = "bottomLeft" case BottomCenter = "bottomCenter" case BottomRight = "bottomRight" - + static func mapToRNAlignment(value: String) -> RNAlignment { if let rnEnum = RNAlignment(rawValue: value) { return rnEnum } else { - fatalError("Unsupported alignment type: \(value)") + // Return a default value instead of crashing + RCTLogWarn("Unsupported alignment type: \(value), defaulting to Center") + return .Center } } - + static func mapToRiveAlignment(rnAlignment: RNAlignment) -> RiveAlignment { switch rnAlignment { case .TopLeft: diff --git a/ios/RNDirection.swift b/ios/RNDirection.swift index 84b3e925..3d7395f7 100644 --- a/ios/RNDirection.swift +++ b/ios/RNDirection.swift @@ -2,15 +2,17 @@ enum RNDirection: String { case Backwards = "backwards" case Auto = "auto" case Forwards = "forwards" - + static func mapToRNDirection(value: String) -> RNDirection { if let rnEnum = RNDirection(rawValue: value) { return rnEnum } else { - fatalError("Unsupported direction type: \(value)") + // Return a default value instead of crashing + RCTLogWarn("Unsupported direction type: \(value), defaulting to Auto") + return .Auto } } - + static func mapToRiveDirection(rnDirection: RNDirection) -> RiveDirection { switch rnDirection { case .Backwards: diff --git a/ios/RNFit.swift b/ios/RNFit.swift index c7325c1d..35aff8e2 100644 --- a/ios/RNFit.swift +++ b/ios/RNFit.swift @@ -10,15 +10,17 @@ enum RNFit: String { case None = "none" case ScaleDown = "scaleDown" case Layout = "layout" - + static func mapToRNFit(value: String) -> RNFit { if let rnEnum = RNFit(rawValue: value) { return rnEnum } else { - fatalError("Unsupported fit type: \(value)") + // Return a default value instead of crashing + RCTLogWarn("Unsupported fit type: \(value), defaulting to Contain") + return .Contain } } - + static func mapToRiveFit(rnFit: RNFit) -> RiveFit { switch rnFit { case .Contain: diff --git a/ios/RNLoopMode.swift b/ios/RNLoopMode.swift index bafab2bd..48ad4372 100644 --- a/ios/RNLoopMode.swift +++ b/ios/RNLoopMode.swift @@ -3,16 +3,18 @@ enum RNLoopMode: String { case Loop = "loop" case PingPong = "pingPong" case Auto = "auto" - + static func mapToRNLoopMode(value: String) -> RNLoopMode { if let rnEnum = RNLoopMode(rawValue: value) { return rnEnum } else { - fatalError("Unsupported loop mode type: \(value)") + // Return a default value instead of crashing + RCTLogWarn("Unsupported loop mode type: \(value), defaulting to Auto") + return .Auto } } - - + + static func mapToRNLoopMode(value: Int) -> RNLoopMode { if let riveEnum = RiveRuntime.RiveLoop(rawValue: value) { switch (riveEnum) { @@ -27,13 +29,15 @@ enum RNLoopMode: String { default: return .Auto } - - + + } else { - fatalError("Unsupported loop mode type: \(value)") + // Return a default value instead of crashing + RCTLogWarn("Unsupported loop mode type: \(value), defaulting to Auto") + return .Auto } } - + static func mapToRiveLoop(rnLoopMode: RNLoopMode) -> RiveLoop { switch rnLoopMode { case .OneShot: diff --git a/ios/RNPropertyType.swift b/ios/RNPropertyType.swift index d08d87fa..295d4392 100644 --- a/ios/RNPropertyType.swift +++ b/ios/RNPropertyType.swift @@ -14,11 +14,12 @@ enum RNPropertyType: String { case Color = "color" case Trigger = "trigger" case Enum = "enum" - + static func mapToRNPropertyType(value: String) -> RNPropertyType? { if let rnEnum = RNPropertyType(rawValue: value) { return rnEnum } else { + RCTLogWarn("Unsupported property type: \(value)") return nil } } diff --git a/ios/RNRiveError.swift b/ios/RNRiveError.swift index ae953fcb..1654b744 100644 --- a/ios/RNRiveError.swift +++ b/ios/RNRiveError.swift @@ -1,3 +1,10 @@ +enum RiveErrorCode: Int { + case malformedFile = 100 + case fileNotFound = 800 + case assetFileError = 801 + case incorrectRiveURL = 900 +} + struct BaseRNRiveError { let type: String; var message: String = "Default Message" @@ -15,10 +22,16 @@ struct RNRiveError { static let IncorrectStateMachineInput = BaseRNRiveError(type: "IncorrectStateMachineInput") static let TextRunNotFoundError = BaseRNRiveError(type: "TextRunNotFoundError") static let DataBindingError = BaseRNRiveError(type: "DataBindingError") - - + + static func mapToRNRiveError(riveError: NSError) -> BaseRNRiveError? { - let riveErrorName = riveError.userInfo["name"] as! String + guard let riveErrorName = riveError.userInfo["name"] as? String else { + // If we can't get the error name, return a generic error with the description + var genericError = BaseRNRiveError(type: "UnknownError") + genericError.message = riveError.localizedDescription + return genericError + } + var resultError: BaseRNRiveError? = nil switch riveErrorName { case "UnsupportedVersion": @@ -50,7 +63,7 @@ struct RNRiveError { break; case "DataBindingError": resultError = RNRiveError.DataBindingError - break; + break; default: return nil } @@ -61,7 +74,7 @@ struct RNRiveError { func createFileNotFoundError() -> NSError { - return NSError(domain: RiveErrorDomain, code: 800, userInfo: [NSLocalizedDescriptionKey: "File not found", "name": "FileNotFound"]) + return NSError(domain: RiveErrorDomain, code: RiveErrorCode.fileNotFound.rawValue, userInfo: [NSLocalizedDescriptionKey: "File not found", "name": "FileNotFound"]) } func createMalformedFileError() -> NSError { @@ -69,9 +82,9 @@ func createMalformedFileError() -> NSError { } func createAssetFileError(_ assetName: String) -> NSError { - return NSError(domain: RiveErrorDomain, code: 801, userInfo: [NSLocalizedDescriptionKey: "Could not load Rive asset: \(assetName)", "name": "FileNotFound"]) + return NSError(domain: RiveErrorDomain, code: RiveErrorCode.assetFileError.rawValue, userInfo: [NSLocalizedDescriptionKey: "Could not load Rive asset: \(assetName)", "name": "FileNotFound"]) } func createIncorrectRiveURL(_ url: String) -> NSError { - return NSError(domain: RiveErrorDomain, code: 900, userInfo: [NSLocalizedDescriptionKey: "Unable to download Rive file from: \(url)", "name": "IncorrectRiveFileURL"]) + return NSError(domain: RiveErrorDomain, code: RiveErrorCode.incorrectRiveURL.rawValue, userInfo: [NSLocalizedDescriptionKey: "Unable to download Rive file from: \(url)", "name": "IncorrectRiveFileURL"]) } diff --git a/ios/RNRiveRendererType.swift b/ios/RNRiveRendererType.swift index 4f569764..01023b4f 100644 --- a/ios/RNRiveRendererType.swift +++ b/ios/RNRiveRendererType.swift @@ -16,7 +16,8 @@ enum RNRiveRendererType: String { if let rnEnum = RNRiveRendererType(rawValue: value) { return rnEnum } else { - fatalError("Unsupported renderer type: \(value)") + RCTLogWarn("Unsupported renderer type: \(value), defaulting to Rive") + return .Rive } } diff --git a/ios/RiveReactNativeView.swift b/ios/RiveReactNativeView.swift index 4a218ed1..54960f8a 100644 --- a/ios/RiveReactNativeView.swift +++ b/ios/RiveReactNativeView.swift @@ -9,7 +9,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } private var propertyListeners: [String: PropertyListener] = [:] private var dataBindingConfig: DataBindingConfig? - + // MARK: RiveReactNativeView Properties private var resourceFromBundle = true private var requiresLocalResourceReconfigure = false @@ -24,7 +24,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate @objc var onRiveEventReceived: RCTDirectEventBlock? @objc var onError: RCTDirectEventBlock? @objc var isUserHandlingErrors: Bool - + // MARK: RiveRuntime Bindings var riveView: RiveView? var viewModel: RiveViewModel? @@ -32,7 +32,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate var cachedRiveFactory: RiveFactory? var previousReferencedAssets: NSDictionary? var cachedFileAssets: [String: RiveFileAsset] = [:] - + @objc var resourceName: String? = nil { didSet { if (resourceName != nil) { @@ -42,7 +42,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + @objc var url: String? = nil { didSet { if (url != nil) { @@ -51,13 +51,13 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + @objc var fit: String? - + @objc var layoutScaleFactor: NSNumber = -1.0 // -1.0 will inform the iOS runtime to determine the correct scale factor automatically - + @objc var alignment: String? - + @objc var autoplay: Bool { didSet { @@ -66,9 +66,9 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + @objc var artboardName: String? - + @objc var referencedAssets: NSDictionary? { didSet { guard referencedAssets != previousReferencedAssets else { return } @@ -76,7 +76,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate previousReferencedAssets = referencedAssets } } - + @objc var dataBinding: [String: Any]? { didSet { guard let type = dataBinding?["type"] as? String else { return } @@ -103,34 +103,34 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate }() } } - + @objc var animationName: String? - - + + @objc var stateMachineName: String? - - + + override init(frame: CGRect) { self.autoplay = false // will be changed by react native self.isUserHandlingErrors = false super.init(frame: frame) } - + required init?(coder aDecoder: NSCoder) { self.autoplay = true self.isUserHandlingErrors = false super.init(coder: aDecoder) - fatalError("init(coder:) has not been implemented") + RCTLogError("RiveReactNativeView init(coder:) is not supported and may not work correctly") } - + // MARK: - React Native Helpers - + override func removeFromSuperview() { cleanupResources() - + super.removeFromSuperview() } - + private func cleanupResources() { cleanupDataBinding() cleanupFileAssetCache() @@ -142,7 +142,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate viewModel?.deregisterView(); viewModel = nil; } - + private func cleanupDataBinding() { if let loadedTag = generateLoadedTag() { eventEmitter?.removeListener(byName: loadedTag) @@ -154,36 +154,36 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate propertyListeners.removeAll() dataBindingViewModelInstance = nil } - + private func cleanupFileAssetCache() { cachedFileAssets.removeAll() cachedRiveFactory = nil } - + override func layoutSubviews() { super.layoutSubviews() for view in subviews { view.reactSetFrame(self.bounds) } } - + override func didSetProps(_ changedProps: [String]!) { if (changedProps.contains("url") || changedProps.contains("resourceName") || changedProps.contains("artboardName") || changedProps.contains("animationName") || changedProps.contains("stateMachineName") || changedProps.contains("referencedAssets")) { reloadView() } - + if (changedProps.contains("fit")) { viewModel?.fit = convertFit(fit) } - + if (changedProps.contains("alignment")) { viewModel?.alignment = convertAlignment(alignment) } - + if (changedProps.contains("layoutScaleFactor")) { viewModel?.layoutScaleFactor = layoutScaleFactor.doubleValue } - + if (changedProps.contains("dataBinding")) { if let viewModel = viewModel { configureDataBinding(viewModel: viewModel, dataBindingConfig: dataBindingConfig) @@ -192,7 +192,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + private func convertFit(_ fit: String? = nil) -> RiveFit { if let safeFit = fit { let rnFit = RNFit.mapToRNFit(value: safeFit) @@ -200,7 +200,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } return RiveFit.contain } - + private func convertAlignment(_ alignment: String? = nil) -> RiveAlignment { if let safeAlignment = alignment { let rnAlignment = RNAlignment.mapToRNAlignment(value: safeAlignment) @@ -208,7 +208,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } return RiveAlignment.center } - + private func safePropertyType(_ propertyType: String? = nil) -> RNPropertyType? { if let safePropertyType = propertyType { let rnPropertyType = RNPropertyType.mapToRNPropertyType(value: safePropertyType) @@ -221,7 +221,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate dataBindingConfigState = .configured guard let artboard = viewModel.riveModel?.artboard, let dataBindingViewModel = viewModel.riveModel?.riveFile.defaultViewModel(for: artboard) else { return } - + func bindInstance(_ instance: RiveDataBindingViewModel.Instance?) { guard let instance = instance else { var error = RNRiveError.DataBindingError @@ -245,7 +245,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } viewModel.riveModel?.stateMachine?.bind(viewModelInstance: instance) self.dataBindingViewModelInstance = instance - + // As we can't control whether `configureDataBinding` is called // before/after/between `registerPropertyListener` (if it is called again) we // re-add the current registered listeners if the instance is not the same @@ -256,7 +256,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + switch dataBindingConfig { case .autoBind(let autoBind): if autoBind { @@ -281,7 +281,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate break } } - + private func createNewView(updatedViewModel : RiveViewModel){ riveView?.playerDelegate = nil riveView?.stateMachineDelegate = nil @@ -292,14 +292,23 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate configureDataBinding(viewModel: updatedViewModel, dataBindingConfig: config) } viewModel = updatedViewModel - riveView = viewModel!.createRiveView() - addSubview(riveView!) - riveView?.playerDelegate = self - riveView?.stateMachineDelegate = self + guard let newViewModel = viewModel else { + RCTLogError("Failed to set view model") + return + } - sendRiveLoadedEvent() + riveView = newViewModel.createRiveView() + if let newRiveView = riveView { + addSubview(newRiveView) + newRiveView.playerDelegate = self + newRiveView.stateMachineDelegate = self + + sendRiveLoadedEvent() + } else { + RCTLogError("Failed to create Rive view") + } } - + // Helper function to generate the loaded evet tag that is sent to JS // Part of the `useRive()` hook. private func generateLoadedTag() -> String? { @@ -308,21 +317,21 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } return "RiveReactNativeLoaded:\(reactTag)" } - + // Send the "RiveReactNativeLoaded" event private func sendRiveLoadedEvent() { guard let loadedTag = generateLoadedTag(), eventEmitter?.isListenerActive(loadedTag) == true else { return } eventEmitter?.sendEvent(withName: loadedTag, body: nil) } - + private func configureViewModelFromResource() { cleanupFileAssetCache() - + 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, customLoader: customLoader) @@ -331,14 +340,14 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } else { updatedViewModel = RiveViewModel(fileName: name, fit: convertFit(fit), alignment: convertAlignment(alignment), autoPlay: autoplay, artboardName: artboardName, customLoader: customLoader) } - + updatedViewModel.layoutScaleFactor = layoutScaleFactor.doubleValue - + createNewView(updatedViewModel: updatedViewModel) requiresLocalResourceReconfigure = false } } - + private func configureViewModelFromUrl() { guard let url = url else { handleRiveError(error: createIncorrectRiveURL(url ?? "")) @@ -378,86 +387,95 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } - + private func reloadView() { if resourceFromBundle { if requiresLocalResourceReconfigure { configureViewModelFromResource() return; // exit early, new RiveViewModel created, no need to configure further } - + do { try viewModel?.configureModel(artboardName: artboardName, stateMachineName: stateMachineName, animationName: animationName) } catch let error as NSError { handleRiveError(error: error) } - + } else { configureViewModelFromUrl() // TODO: calling viewModel?.configureModel for a URL ViewModel throws. Requires further investigation. Currently recreating the whole ViewModel for certain prop changes. } } - + private func updateReferencedAssets(incomingReferencedAssets: NSDictionary?) { guard let referencedAssets = incomingReferencedAssets?.copy() as? NSDictionary, let cachedReferencedAssets = previousReferencedAssets?.copy() as? NSDictionary else { return } - - let referencedKeys = Set(referencedAssets.allKeys as! [String]) - let cachedKeys = Set(cachedReferencedAssets.allKeys as! [String]) - + + guard let referencedKeysArray = referencedAssets.allKeys as? [String], + let cachedKeysArray = cachedReferencedAssets.allKeys as? [String] else { + RCTLogError("Failed to convert dictionary keys to strings") + return + } + + let referencedKeys = Set(referencedKeysArray) + let cachedKeys = Set(cachedKeysArray) + // The keys are different, reloading the whole file if referencedKeys != cachedKeys { requiresLocalResourceReconfigure = true return } - + var hasChanged = false for (key, value) in referencedAssets { guard let keyString = key as? String, let cachedValue = cachedReferencedAssets[keyString] as? NSDictionary, - let newValue = value as? NSDictionary, - !cachedValue.isEqual(to: newValue as! [AnyHashable : Any]) else { + let newValue = value as? NSDictionary else { continue } - - hasChanged = true - if let source = newValue["source"] as? NSDictionary, - let asset = cachedFileAssets[keyString], - let factory = cachedRiveFactory { - loadAsset(source: source, asset: asset, factory: factory) + + if let newValueDict = newValue as? [AnyHashable: Any], + !cachedValue.isEqual(to: newValueDict) { + + hasChanged = true + if let source = newValue["source"] as? NSDictionary, + let asset = cachedFileAssets[keyString], + let factory = cachedRiveFactory { + loadAsset(source: source, asset: asset, factory: factory) + } } } - + if hasChanged && viewModel?.isPlaying == false { viewModel?.play() // manually calling play to force an update, ideally want to do a single advance } } - + private func customLoader(asset: RiveFileAsset, data: Data, factory: RiveFactory) -> Bool { guard let assetData = referencedAssets?[asset.uniqueName()] as? NSDictionary ?? referencedAssets?[asset.name()] as? NSDictionary else { return false } let usedKey = referencedAssets?[asset.uniqueName()] != nil ? asset.uniqueName() : asset.name() - + cachedRiveFactory = factory if cachedFileAssets[usedKey] == nil { cachedFileAssets[usedKey] = asset } - + if let source = assetData["source"] as? NSDictionary { loadAsset(source: source, asset: asset, factory: factory) return true } - + return false } - + private func loadAsset(source: NSDictionary, asset: RiveFileAsset, factory: RiveFactory) { let sourceAssetId = source["sourceAssetId"] as? String let sourceUrl = source["sourceUrl"] as? String let sourceAsset = source["sourceAsset"] as? String - + if let sourceAssetId = sourceAssetId { handleSourceAssetId(sourceAssetId, asset: asset, factory: factory) } else if let sourceUrl = sourceUrl { @@ -466,29 +484,29 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate handleSourceAsset(sourceAsset, path: source["path"] as? String, asset: asset, factory: factory) } } - + private func handleSourceAssetId(_ sourceAssetId: String, asset: RiveFileAsset, factory: RiveFactory) { guard URL(string: sourceAssetId) != nil else { return } - + downloadUrlAsset(url: sourceAssetId) { [weak self] data in self?.processAssetBytes(data, asset: asset, factory: factory) } } - + private func handleSourceUrl(_ sourceUrl: String, asset: RiveFileAsset, factory: RiveFactory) { downloadUrlAsset(url: sourceUrl) { [weak self] data in self?.processAssetBytes(data, asset: asset, factory: factory) } } - + private func handleSourceAsset(_ sourceAsset: String, path: String?, asset: RiveFileAsset, factory: RiveFactory) { loadResourceAsset(sourceAsset: sourceAsset, path: path) {[weak self] data in self?.processAssetBytes(data, asset: asset, factory: factory) } } - + private func processAssetBytes(_ data: Data, asset: RiveFileAsset, factory: RiveFactory) { if (data.isEmpty == true) { return; @@ -515,19 +533,19 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + private func downloadUrlAsset(url: String, listener: @escaping (Data) -> Void) { guard isValidUrl(url) else { handleInvalidUrlError(url: url) return } - + let queue = URLSession.shared guard let requestUrl = URL(string: url) else { handleInvalidUrlError(url: url) return } - + let request = URLRequest(url: requestUrl) let task = queue.dataTask(with: request) {[weak self] data, response, error in if error != nil { @@ -536,10 +554,10 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate listener(data) } } - + task.resume() } - + private func isValidUrl(_ url: String) -> Bool { if let url = URL(string: url) { return UIApplication.shared.canOpenURL(url) @@ -547,7 +565,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate return false } } - + private func splitFileNameAndExtension(fileName: String) -> (name: String?, ext: String?)? { let components = fileName.split(separator: ".") let name = (fileName as NSString).deletingPathExtension; @@ -555,7 +573,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate guard components.count == 2 else { return nil } return (name: name, ext: fileExtension) } - + private func loadResourceAsset(sourceAsset: String, path: String?, listener: @escaping (Data) -> Void) { guard let splitSourceAssetName = splitFileNameAndExtension(fileName: sourceAsset), let name = splitSourceAssetName.name, @@ -563,12 +581,12 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate handleRiveError(error: createAssetFileError(sourceAsset)) return } - + guard let folderUrl = Bundle.main.url(forResource: name, withExtension: ext) else { handleRiveError(error: createAssetFileError(sourceAsset)) return } - + DispatchQueue.global(qos: .background).async { [weak self] in do { let fileData = try Data(contentsOf: folderUrl) @@ -582,13 +600,13 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } } - + private func handleInvalidUrlError(url: String) { handleRiveError(error: createIncorrectRiveURL(url)) } - + // MARK: - Playback Controls - + func play(animationName: String? = nil, rnLoopMode: RNLoopMode, rnDirection: RNDirection, isStateMachine: Bool) { let loop = RNLoopMode.mapToRiveLoop(rnLoopMode: rnLoopMode) let direction = RNDirection.mapToRiveDirection(rnDirection: rnDirection) @@ -598,64 +616,78 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate viewModel?.play(animationName: animationName, loop: loop, direction: direction) } } - + func pause() { viewModel?.pause() } - + func stop() { viewModel?.stop() } - + func reset() { viewModel?.reset() reloadView() } - + // MARK: - StateMachine Inputs - + func fireState(stateMachineName: String, inputName: String) { viewModel?.triggerInput(inputName) } - + func setNumberState(stateMachineName: String, inputName: String, value: Float) { viewModel?.setInput(inputName, value: value) } - + func getBooleanState(inputName: String) -> Bool? { - return viewModel?.boolInput(named: inputName)?.value(); + let input = viewModel?.boolInput(named: inputName) + if input == nil { + RCTLogWarn("Failed to get boolean input '\(inputName)'") + } + return input?.value(); } - + func getNumberState(inputName: String) -> Float? { - return viewModel?.numberInput(named: inputName)?.value(); + let input = viewModel?.numberInput(named: inputName) + if input == nil { + RCTLogWarn("Failed to get number input '\(inputName)'") + } + return input?.value(); } - + func getBooleanStateAtPath(inputName: String, path: String) -> Bool? { let input = viewModel?.riveModel?.artboard?.getBool(inputName, path: path); + if input == nil { + RCTLogWarn("Failed to get boolean input '\(inputName)' at path '\(path)'") + } return input?.value(); } - + func getNumberStateAtPath(inputName: String, path: String) -> Float? { let input = viewModel?.riveModel?.artboard?.getNumber(inputName, path: path); + if input == nil { + RCTLogWarn("Failed to get number input '\(inputName)' at path '\(path)'") + } return input?.value(); } - + func setBooleanState(stateMachineName: String, inputName: String, value: Bool) { viewModel?.setInput(inputName, value: value) } - + func fireStateAtPath(inputName: String, path: String) { viewModel?.triggerInput(inputName, path: path) } - + func setNumberStateAtPath(inputName: String, value: Float, path: String) { viewModel?.setInput(inputName, value: value, path: path) } - + func setBooleanStateAtPath(inputName: String, value: Bool, path: String) { viewModel?.setInput(inputName, value: value, path: path) } - + // MARK: - Text Runs func setTextRunValue(textRunName: String, textRunValue: String) throws { do { @@ -664,7 +696,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate handleRiveError(error: error) } } - + func setTextRunValueAtPath(textRunName: String, textRunValue: String, path: String) throws { do { try viewModel?.setTextRunValue(textRunName, path: path, textValue: textRunValue) @@ -672,52 +704,52 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate handleRiveError(error: error) } } - + // MARK: - Data Binding func setBooleanPropertyValue(path: String, value: Bool) { dataBindingViewModelInstance?.booleanProperty(fromPath: path)?.value = value } - + func setStringPropertyValue(path: String, value: String) { dataBindingViewModelInstance?.stringProperty(fromPath: path)?.value = value } - + func setNumberPropertyValue(path: String, value: Float) { dataBindingViewModelInstance?.numberProperty(fromPath: path)?.value = value } - + func setColorPropertyValue(path: String, r: Int, g: Int, b: Int, a: Int) { dataBindingViewModelInstance?.colorProperty(fromPath: path)?.value = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: CGFloat(a) / 255.0) } - + func setEnumPropertyValue(path: String, value: String) { dataBindingViewModelInstance?.enumProperty(fromPath: path)?.value = value } - + func fireTriggerProperty(path: String) { dataBindingViewModelInstance?.triggerProperty(fromPath: path)?.trigger() } - + private func storeProperty(key: String, propertyListener: PropertyListener) { if let existingListener = propertyListeners[key]?.listener, let existingProperty = propertyListeners[key]?.property { existingProperty.removeListener(existingListener) } propertyListeners[key] = propertyListener } - + private struct PropertyRegistration { let property: RiveDataBindingViewModel.Instance.Property let initialValue: Any? let createListener: () -> UUID? } - + func registerPropertyListener(path: String, propertyType: String) { guard let reactTag = self.reactTag, let dataBindingInstance = dataBindingViewModelInstance, let propertyTypeEnum = safePropertyType(propertyType) else { return } - + let key = "\(propertyType):\(path):\(reactTag)" - + // Get registration info based on property type let registration: PropertyRegistration? = { switch propertyTypeEnum { @@ -732,7 +764,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } ) - + case .Boolean: guard let prop = dataBindingInstance.booleanProperty(fromPath: path) else { return nil } return PropertyRegistration( @@ -744,7 +776,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } ) - + case .Number: guard let prop = dataBindingInstance.numberProperty(fromPath: path) else { return nil } return PropertyRegistration( @@ -756,7 +788,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } ) - + case .Color: guard let prop = dataBindingInstance.colorProperty(fromPath: path) else { return nil } return PropertyRegistration( @@ -768,7 +800,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } ) - + case .Enum: guard let prop = dataBindingInstance.enumProperty(fromPath: path) else { return nil } return PropertyRegistration( @@ -780,7 +812,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } } ) - + case .Trigger: guard let prop = dataBindingInstance.triggerProperty(fromPath: path) else { return nil } return PropertyRegistration( @@ -794,19 +826,19 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate ) } }() - + guard let registration else { var error = RNRiveError.DataBindingError; error.message = "\(propertyType) property not found at path: \(path)" onRNRiveError(error) return } - + // Send initial value if let initialValue = registration.initialValue { eventEmitter?.sendEvent(withName: key, body: initialValue) } - + // Create and store listener if let listener = registration.createListener() { let propertyListener = PropertyListener( @@ -819,16 +851,16 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate storeProperty(key: key, propertyListener: propertyListener) } } - + // MARK: - StateMachineDelegate - + @objc func stateMachine(_ stateMachine: RiveStateMachineInstance, didChangeState stateName: String) { onStateChanged?(["stateMachineName": stateMachine.name(), "stateName": stateName]) } - + @objc func stateMachine(_ stateMachine: RiveStateMachineInstance, receivedInput input: StateMachineInput) { } - + @objc func onRiveEventReceived(onRiveEvent riveEvent: RiveEvent) { // Need to convert NSObject to Dictionary so React Native can support the serialization to JS // Might be a better way to convert NSObject -> Dictionary in the future @@ -844,9 +876,9 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate } onRiveEventReceived?(["riveEvent": eventDict]) } - + // MARK: - PlayerDelegate - + func player(playedWithModel riveModel: RiveModel?) { if (riveModel?.animation != nil || riveModel?.stateMachine != nil) { onPlay?([ @@ -855,70 +887,97 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate ]) } } - + func player(pausedWithModel riveModel: RiveModel?) { onPause?([ "animationName": riveModel?.animation?.name() ?? riveModel?.stateMachine?.name() ?? "", "isStateMachine": riveModel?.stateMachine != nil ]) } - + func player(loopedWithModel riveModel: RiveModel?, type: Int) { onLoopEnd?([ "animationName": riveModel?.animation?.name() ?? "", "loopMode": RNLoopMode.mapToRNLoopMode(value: type).rawValue ]) } - + func player(stoppedWithModel riveModel: RiveModel?) { onStop?([ "animationName": riveModel?.animation?.name() ?? riveModel?.stateMachine?.name() ?? "", "isStateMachine": riveModel?.stateMachine != nil ]) } - + func player(didAdvanceby seconds: Double, riveModel: RiveModel?) { // TODO: implement if in Android } - + // MARK: - Touch Events - + @objc open func touchBegan(_ location: CGPoint) { handleTouch(location: location) { machine, abLocation in - guard let riveView = viewModel?.riveView else { return } - guard let artboard = viewModel?.riveModel?.artboard else { return } + guard let riveView = viewModel?.riveView else { + RCTLogWarn("Touch began: RiveView is nil") + return + } + guard let artboard = viewModel?.riveModel?.artboard else { + RCTLogWarn("Touch began: Artboard is nil") + return + } if (riveView.stateMachineDelegate?.touchBegan != nil) { riveView.stateMachineDelegate?.touchBegan?(onArtboard: artboard, atLocation: abLocation) } } } - + @objc open func touchMoved(_ location: CGPoint) { handleTouch(location: location) { machine, abLocation in - guard let riveView = viewModel?.riveView else { return } - guard let artboard = viewModel?.riveModel?.artboard else { return } + guard let riveView = viewModel?.riveView else { + RCTLogWarn("Touch moved: RiveView is nil") + return + } + guard let artboard = viewModel?.riveModel?.artboard else { + RCTLogWarn("Touch moved: Artboard is nil") + return + } riveView.stateMachineDelegate?.touchMoved?(onArtboard: artboard, atLocation: abLocation) } } - + @objc open func touchEnded(_ location: CGPoint) { handleTouch(location: location) { machine, abLocation in - guard let riveView = viewModel?.riveView else { return } - guard let artboard = viewModel?.riveModel?.artboard else { return } + guard let riveView = viewModel?.riveView else { + RCTLogWarn("Touch ended: RiveView is nil") + return + } + guard let artboard = viewModel?.riveModel?.artboard else { + RCTLogWarn("Touch ended: Artboard is nil") + return + } riveView.stateMachineDelegate?.touchEnded?(onArtboard: artboard, atLocation: abLocation) } } - + @objc open func touchCancelled(_ location: CGPoint) { handleTouch(location: location) { machine, abLocation in - guard let riveView = viewModel?.riveView else { return } - guard let artboard = viewModel?.riveModel?.artboard else { return } + guard let riveView = viewModel?.riveView else { + RCTLogWarn("Touch cancelled: RiveView is nil") + return + } + guard let artboard = viewModel?.riveModel?.artboard else { + RCTLogWarn("Touch cancelled: Artboard is nil") + return + } riveView.stateMachineDelegate?.touchCancelled?(onArtboard: artboard, atLocation: abLocation) } } - + private func handleTouch(location: CGPoint, action: (RiveStateMachineInstance, CGPoint)->Void) { - guard let bounds = viewModel?.riveModel?.artboard?.bounds() else { return } + guard let bounds = viewModel?.riveModel?.artboard?.bounds() else { + RCTLogWarn("Handle touch: Artboard bounds are nil") + return + } if let viewModel = viewModel, let riveView = viewModel.riveView { let artboardLocation = riveView.artboardLocation( fromTouchLocation: location, @@ -929,16 +988,20 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate if let stateMachine = viewModel.riveModel?.stateMachine { viewModel.play() action(stateMachine, artboardLocation) + } else { + RCTLogWarn("Handle touch: State machine is nil") } + } else { + RCTLogWarn("Handle touch: ViewModel or RiveView is nil") } } - + // MARK: - Error Handling - + private func onRNRiveError(_ rnRiveError: BaseRNRiveError) { onError?(["type": rnRiveError.type, "message": rnRiveError.message]) } - + private func handleRiveError(error: NSError) { if isUserHandlingErrors { let rnRiveError = RNRiveError.mapToRNRiveError(riveError: error) @@ -949,7 +1012,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate RCTLogError(error.localizedDescription) } } - + private enum DataBindingConfig { case autoBind(Bool) case index(Int) @@ -978,14 +1041,14 @@ extension UIColor { var green: CGFloat = 0 var blue: CGFloat = 0 var alpha: CGFloat = 0 - + self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) - + let r = UInt32(red * 255) let g = UInt32(green * 255) let b = UInt32(blue * 255) let a = UInt32(alpha * 255) - + return Int((a << 24) | (r << 16) | (g << 8) | b) } } diff --git a/ios/RiveReactNativeViewManager.swift b/ios/RiveReactNativeViewManager.swift index 76a0c756..64960f98 100644 --- a/ios/RiveReactNativeViewManager.swift +++ b/ios/RiveReactNativeViewManager.swift @@ -1,168 +1,236 @@ -import UIKit +import Foundation import RiveRuntime @objc(RiveReactNativeViewManager) class RiveReactNativeViewManager: RCTViewManager { - + override func view() -> UIView! { let view = RiveReactNativeView() view.bridge = self.bridge return view } - + @objc func play(_ node: NSNumber, animationName: String, loop: String, direction: String, isStateMachine: Bool) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for play") + return + } component.play(animationName: animationName, rnLoopMode: RNLoopMode.mapToRNLoopMode(value: loop), rnDirection: RNDirection.mapToRNDirection(value: direction), isStateMachine: isStateMachine); - + } } - + @objc func pause(_ node: NSNumber) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for pause") + return + } component.pause() } } - + @objc func stop(_ node: NSNumber) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for stop") + return + } component.stop() } } - + @objc func reset(_ node: NSNumber) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for reset") + return + } component.reset() } } - + @objc func fireState(_ node: NSNumber, stateMachineName: String, inputName: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for fireState") + return + } component.fireState(stateMachineName: stateMachineName, inputName: inputName) } } - + @objc func setBooleanState(_ node: NSNumber, stateMachineName: String, inputName: String, value: Bool) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setBooleanState") + return + } component.setBooleanState(stateMachineName: stateMachineName, inputName: inputName, value: value) } } - + @objc func setNumberState(_ node: NSNumber, stateMachineName: String, inputName: String, value: NSNumber) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setNumberState") + return + } component.setNumberState(stateMachineName: stateMachineName, inputName: inputName, value: Float(truncating: value)) } } - + @objc func fireStateAtPath(_ node: NSNumber, inputName: String, path: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for fireStateAtPath") + return + } component.fireStateAtPath(inputName: inputName, path: path) } } - + @objc func setBooleanStateAtPath(_ node: NSNumber, inputName: String, value: Bool, path: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setBooleanStateAtPath") + return + } component.setBooleanStateAtPath(inputName: inputName, value: value, path: path) } } - + @objc func setNumberStateAtPath(_ node: NSNumber, inputName: String, value: NSNumber, path: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setNumberStateAtPath") + return + } component.setNumberStateAtPath(inputName: inputName, value: Float(truncating: value), path: path) } } - + @objc func touchBegan(_ node: NSNumber, x: NSNumber, y: NSNumber) { DispatchQueue.main.async { - let view = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let view = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for touchBegan") + return + } let touch = CGPoint(x: x.doubleValue, y: y.doubleValue) view.touchBegan(touch) } } - + @objc func touchEnded(_ node: NSNumber, x: NSNumber, y: NSNumber) { DispatchQueue.main.async { - let view = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let view = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for touchEnded") + return + } let touch = CGPoint(x: x.doubleValue, y: y.doubleValue) view.touchEnded(touch) } } - + @objc func setTextRunValue(_ node: NSNumber, textRunName: String, textRunValue: String) { DispatchQueue.main.async { - let view = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView - try! view.setTextRunValue(textRunName: textRunName, textRunValue: textRunValue) + guard let view = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setTextRunValue") + return + } + do { + try view.setTextRunValue(textRunName: textRunName, textRunValue: textRunValue) + } catch { + RCTLogError("Failed to set text run value: \(error.localizedDescription)") + } } } - + @objc func setTextRunValueAtPath(_ node: NSNumber, textRunName: String, textRunValue: String, path: String) { DispatchQueue.main.async { - let view = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView - try! view.setTextRunValueAtPath(textRunName: textRunName, textRunValue: textRunValue, path: path) + guard let view = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setTextRunValueAtPath") + return + } + do { + try view.setTextRunValueAtPath(textRunName: textRunName, textRunValue: textRunValue, path: path) + } catch { + RCTLogError("Failed to set text run value at path: \(error.localizedDescription)") + } } } - + @objc func setBooleanPropertyValue(_ node: NSNumber, path: String, value: Bool) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setBooleanPropertyValue") + return + } component.setBooleanPropertyValue(path: path, value: value) } } - + @objc func setStringPropertyValue(_ node: NSNumber, path: String, value: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setStringPropertyValue") + return + } component.setStringPropertyValue(path: path, value: value) } } - + @objc func setNumberPropertyValue(_ node: NSNumber, path: String, value: NSNumber) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setNumberPropertyValue") + return + } component.setNumberPropertyValue(path: path, value: Float(truncating: value)) } } - + @objc func setColorPropertyValue(_ node: NSNumber, path: String, r: NSNumber, g: NSNumber, b: NSNumber, a: NSNumber) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setColorPropertyValue") + return + } component.setColorPropertyValue(path: path, r: r.intValue, g: g.intValue, b: b.intValue, a: a.intValue) } } - + @objc func setEnumPropertyValue(_ node: NSNumber, path: String, value: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for setEnumPropertyValue") + return + } component.setEnumPropertyValue(path: path, value: value) } } - + @objc func fireTriggerProperty(_ node: NSNumber, path: String) { DispatchQueue.main.async { - let component = self.bridge.uiManager.view(forReactTag: node) as! RiveReactNativeView + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for fireTriggerProperty") + return + } component.fireTriggerProperty(path: path) } } - + @objc func registerPropertyListener(_ node: NSNumber, path: String, propertyType: String) { DispatchQueue.main.async { - guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView? else { - RCTLogError("Could not cast view to RiveReactNativeView") + guard let component = self.bridge.uiManager.view(forReactTag: node) as? RiveReactNativeView else { + RCTLogError("Could not cast view to RiveReactNativeView for registerPropertyListener") return } - component?.registerPropertyListener(path: path, propertyType: propertyType) + component.registerPropertyListener(path: path, propertyType: propertyType) } } - + @objc static override func requiresMainQueueSetup() -> Bool { return false }