Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions ios/BaseHybridViewModelProperty.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation
import RiveRuntime

/// Protocol for Rive property types that support listener management
protocol RivePropertyWithListeners: AnyObject {
associatedtype ListenerValueType

func addListener(_ callback: @escaping (ListenerValueType) -> Void) -> UUID
func removeListener(_ id: UUID)
}

typealias BooleanPropertyType = RiveDataBindingViewModel.Instance.BooleanProperty
typealias NumberPropertyType = RiveDataBindingViewModel.Instance.NumberProperty
typealias StringPropertyType = RiveDataBindingViewModel.Instance.StringProperty
typealias EnumPropertyType = RiveDataBindingViewModel.Instance.EnumProperty
typealias ColorPropertyType = RiveDataBindingViewModel.Instance.ColorProperty
typealias TriggerPropertyType = RiveDataBindingViewModel.Instance.TriggerProperty
typealias ImagePropertyType = RiveDataBindingViewModel.Instance.ImageProperty

// Make all Rive property types conform to the protocol
extension BooleanPropertyType: RivePropertyWithListeners {
typealias ListenerValueType = Bool // Native: Bool → Bool (no conversion)
}
extension NumberPropertyType: RivePropertyWithListeners {
typealias ListenerValueType = Float // Native: Float → Double (needs conversion)
}
extension StringPropertyType: RivePropertyWithListeners {
typealias ListenerValueType = String // Native: String → String (no conversion)
}
extension EnumPropertyType: RivePropertyWithListeners {
typealias ListenerValueType = String // Native: String → String (no conversion)
}
extension ColorPropertyType: RivePropertyWithListeners {
typealias ListenerValueType = UIColor // Native: UIColor → Double (needs conversion)
}
// Note: TriggerProperty doesn't fit the pattern - it has () -> Void listeners, not (Void) -> Void
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For some classes we won't be able to make use of this abstraction. But I think that is okay, we'll have 80% shared code, and things like Triggers and Lists will need to be custom.


/// Helper class for managing ViewModel property listeners
class PropertyListenerHelper<PropertyType: RivePropertyWithListeners> {
private var listenerIds: [UUID] = []
weak var property: PropertyType?

init(property: PropertyType) {
self.property = property
}

/// Adds a listener to the property and automatically tracks its ID for cleanup
func addListener(_ callback: @escaping (PropertyType.ListenerValueType) -> Void) {
guard let property = property else { return }
let id = property.addListener(callback)
listenerIds.append(id)
}

func removeListeners() throws {
guard let property = property else { return }
for id in listenerIds {
property.removeListener(id)
}
listenerIds.removeAll()
}

func dispose() throws {
try? removeListeners()
}
}

/// Protocol for properties that have typed values (Bool, String, Double, etc.)
/// Provides a default addListener implementation
protocol ValuedPropertyProtocol<ValueType> {
associatedtype PropertyType: RivePropertyWithListeners
associatedtype ValueType

var property: PropertyType! { get }
var helper: PropertyListenerHelper<PropertyType> { get }

func addListener(onChanged: @escaping (ValueType) -> Void) throws
func removeListeners() throws
func dispose() throws
}

/// Default implementations for lifecycle methods (always available)
extension ValuedPropertyProtocol {
func removeListeners() throws {
try helper.removeListeners()
}

func dispose() throws {
try helper.dispose()
}
}

/// Automatic addListener() ONLY when ListenerValueType == ValueType (no conversion needed)
extension ValuedPropertyProtocol where PropertyType.ListenerValueType == ValueType {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We now automatically also handle adding a listener if the native type and the converted type is the same. If not, you have to provide the addListener implementation.

func addListener(onChanged: @escaping (ValueType) -> Void) throws {
helper.addListener(onChanged) // Types match, just forward directly!
}
}
18 changes: 9 additions & 9 deletions ios/HybridViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,42 @@ import RiveRuntime

class HybridViewModel: HybridViewModelSpec {
let viewModel: RiveDataBindingViewModel?

init(viewModel: RiveDataBindingViewModel) {
self.viewModel = viewModel
}

override init() {
self.viewModel = nil
super.init()
}

var propertyCount: Double { Double(viewModel?.propertyCount ?? 0) }

var instanceCount: Double { Double(viewModel?.instanceCount ?? 0) }

var modelName: String { viewModel?.name ?? "" }

func createInstanceByIndex(index: Double) throws -> (any HybridViewModelInstanceSpec)? {
guard let viewModel = viewModel,
let vmi = viewModel.createInstance(fromIndex: UInt(index)) else { return nil }
return HybridViewModelInstance(viewModelInstance: vmi)
}

func createInstanceByName(name: String) throws -> (any HybridViewModelInstanceSpec)? {
guard let viewModel = viewModel,
let vmi = viewModel.createInstance(fromName: name) else { return nil }
return HybridViewModelInstance(viewModelInstance: vmi)
}

func createDefaultInstance() throws -> (any HybridViewModelInstanceSpec)? {
guard let viewModel = viewModel,
let vmi = viewModel.createDefaultInstance() else {
return nil
}
return HybridViewModelInstance(viewModelInstance: vmi)
}

func createInstance() throws -> (any HybridViewModelInstanceSpec)? {
guard let viewModel = viewModel,
let vmi = viewModel.createInstance() else { return nil }
Expand Down
26 changes: 4 additions & 22 deletions ios/HybridViewModelBooleanProperty.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import NitroModules
import RiveRuntime

class HybridViewModelBooleanProperty: HybridViewModelBooleanPropertySpec {
private var property: RiveDataBindingViewModel.Instance.BooleanProperty!
private var listenerIds: [UUID] = []
class HybridViewModelBooleanProperty: HybridViewModelBooleanPropertySpec, ValuedPropertyProtocol {
var property: BooleanPropertyType!
lazy var helper = PropertyListenerHelper(property: property!)

init(property: RiveDataBindingViewModel.Instance.BooleanProperty) {
init(property: BooleanPropertyType) {
self.property = property
super.init()
}
Expand All @@ -26,22 +26,4 @@ class HybridViewModelBooleanProperty: HybridViewModelBooleanPropertySpec {
property.value = newValue
}
}

func addListener(onChanged: @escaping (Bool) -> Void) throws {
let id = property.addListener({ value in
onChanged(value)
})
listenerIds.append(id)
}

func removeListeners() throws {
for id in listenerIds {
property.removeListener(id)
}
listenerIds.removeAll()
}

func dispose() throws {
try? removeListeners()
}
}
25 changes: 7 additions & 18 deletions ios/HybridViewModelColorProperty.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import NitroModules
import RiveRuntime

class HybridViewModelColorProperty: HybridViewModelColorPropertySpec {
private var property: RiveDataBindingViewModel.Instance.ColorProperty!
private var listenerIds: [UUID] = []
class HybridViewModelColorProperty: HybridViewModelColorPropertySpec, ValuedPropertyProtocol {
var property: ColorPropertyType!
lazy var helper = PropertyListenerHelper(property: property!)

init(property: RiveDataBindingViewModel.Instance.ColorProperty) {
init(property: ColorPropertyType) {
self.property = property
super.init()
}
Expand All @@ -27,22 +27,11 @@ class HybridViewModelColorProperty: HybridViewModelColorPropertySpec {
}
}

// Custom addListener because we need to convert UIColor → Double
func addListener(onChanged: @escaping (Double) -> Void) throws {
let id = property.addListener({ value in
onChanged(value.toHexDouble())
})
listenerIds.append(id)
}

func removeListeners() throws {
for id in listenerIds {
property.removeListener(id)
helper.addListener { (color: UIColor) in
onChanged(color.toHexDouble())
}
listenerIds.removeAll()
}

func dispose() throws {
try? removeListeners()
}
}

Expand Down
26 changes: 4 additions & 22 deletions ios/HybridViewModelEnumProperty.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import NitroModules
import RiveRuntime

class HybridViewModelEnumProperty: HybridViewModelEnumPropertySpec {
private var property: RiveDataBindingViewModel.Instance.EnumProperty!
private var listenerIds: [UUID] = []
class HybridViewModelEnumProperty: HybridViewModelEnumPropertySpec, ValuedPropertyProtocol {
var property: EnumPropertyType!
lazy var helper = PropertyListenerHelper(property: property!)

init(property: RiveDataBindingViewModel.Instance.EnumProperty) {
init(property: EnumPropertyType) {
self.property = property
super.init()
}
Expand All @@ -26,22 +26,4 @@ class HybridViewModelEnumProperty: HybridViewModelEnumPropertySpec {
property.value = newValue
}
}

func addListener(onChanged: @escaping (String) -> Void) throws {
let id = property.addListener({ value in
onChanged(value)
})
listenerIds.append(id)
}

func removeListeners() throws {
for id in listenerIds {
property.removeListener(id)
}
listenerIds.removeAll()
}

func dispose() throws {
try? removeListeners()
}
}
18 changes: 9 additions & 9 deletions ios/HybridViewModelInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,43 @@ import RiveRuntime

class HybridViewModelInstance: HybridViewModelInstanceSpec {
let viewModelInstance: RiveDataBindingViewModel.Instance?

init(viewModelInstance: RiveDataBindingViewModel.Instance) {
self.viewModelInstance = viewModelInstance
}

override init() {
self.viewModelInstance = nil
super.init()
}

var instanceName: String { viewModelInstance?.name ?? "" }

func numberProperty(path: String) throws -> (any HybridViewModelNumberPropertySpec)? {
guard let property = viewModelInstance?.numberProperty(fromPath: path) else { return nil }
return HybridViewModelNumberProperty(property: property)
}

func stringProperty(path: String) throws -> (any HybridViewModelStringPropertySpec)? {
guard let property = viewModelInstance?.stringProperty(fromPath: path) else { return nil }
return HybridViewModelStringProperty(property: property)
}

func booleanProperty(path: String) throws -> (any HybridViewModelBooleanPropertySpec)? {
guard let property = viewModelInstance?.booleanProperty(fromPath: path) else { return nil }
return HybridViewModelBooleanProperty(property: property)
}

func colorProperty(path: String) throws -> (any HybridViewModelColorPropertySpec)? {
guard let property = viewModelInstance?.colorProperty(fromPath: path) else { return nil }
return HybridViewModelColorProperty(property: property)
}

func enumProperty(path: String) throws -> (any HybridViewModelEnumPropertySpec)? {
guard let property = viewModelInstance?.enumProperty(fromPath: path) else { return nil }
return HybridViewModelEnumProperty(property: property)
}

func triggerProperty(path: String) throws -> (any HybridViewModelTriggerPropertySpec)? {
guard let property = viewModelInstance?.triggerProperty(fromPath: path) else { return nil }
return HybridViewModelTriggerProperty(property: property)
Expand Down
25 changes: 6 additions & 19 deletions ios/HybridViewModelNumberProperty.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import RiveRuntime

class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec {
var property: RiveDataBindingViewModel.Instance.NumberProperty!
private var listenerIds: [UUID] = []
class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec, ValuedPropertyProtocol {
var property: NumberPropertyType!
lazy var helper = PropertyListenerHelper(property: property!)

init(property: RiveDataBindingViewModel.Instance.NumberProperty) {
init(property: NumberPropertyType) {
self.property = property
super.init()
}
Expand All @@ -26,23 +26,10 @@ class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec {
}
}

// Custom addListener needed because ListenerValueType (Float) != ValueType (Double)
func addListener(onChanged: @escaping (Double) -> Void) throws {
let id = property.addListener({ value in
helper.addListener { (value: Float) in
onChanged(Double(value))
})

listenerIds.append(id)

}

func removeListeners() throws {
for id in listenerIds {
property.removeListener(id)
}
listenerIds.removeAll()
}

func dispose() throws {
try? removeListeners()
}
}
Loading
Loading