Skip to content

Commit 0a58796

Browse files
authored
refactor(ios): shared property listener implementation (#55)
* refactor(ios): shared property listener implementation * refactor: conditional addListener on type match * chore: linting
1 parent 1bddf91 commit 0a58796

9 files changed

Lines changed: 145 additions & 124 deletions
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Foundation
2+
import RiveRuntime
3+
4+
/// Protocol for Rive property types that support listener management
5+
protocol RivePropertyWithListeners: AnyObject {
6+
associatedtype ListenerValueType
7+
8+
func addListener(_ callback: @escaping (ListenerValueType) -> Void) -> UUID
9+
func removeListener(_ id: UUID)
10+
}
11+
12+
typealias BooleanPropertyType = RiveDataBindingViewModel.Instance.BooleanProperty
13+
typealias NumberPropertyType = RiveDataBindingViewModel.Instance.NumberProperty
14+
typealias StringPropertyType = RiveDataBindingViewModel.Instance.StringProperty
15+
typealias EnumPropertyType = RiveDataBindingViewModel.Instance.EnumProperty
16+
typealias ColorPropertyType = RiveDataBindingViewModel.Instance.ColorProperty
17+
typealias TriggerPropertyType = RiveDataBindingViewModel.Instance.TriggerProperty
18+
typealias ImagePropertyType = RiveDataBindingViewModel.Instance.ImageProperty
19+
20+
// Make all Rive property types conform to the protocol
21+
extension BooleanPropertyType: RivePropertyWithListeners {
22+
typealias ListenerValueType = Bool // Native: Bool → Bool (no conversion)
23+
}
24+
extension NumberPropertyType: RivePropertyWithListeners {
25+
typealias ListenerValueType = Float // Native: Float → Double (needs conversion)
26+
}
27+
extension StringPropertyType: RivePropertyWithListeners {
28+
typealias ListenerValueType = String // Native: String → String (no conversion)
29+
}
30+
extension EnumPropertyType: RivePropertyWithListeners {
31+
typealias ListenerValueType = String // Native: String → String (no conversion)
32+
}
33+
extension ColorPropertyType: RivePropertyWithListeners {
34+
typealias ListenerValueType = UIColor // Native: UIColor → Double (needs conversion)
35+
}
36+
// Note: TriggerProperty doesn't fit the pattern - it has () -> Void listeners, not (Void) -> Void
37+
38+
/// Helper class for managing ViewModel property listeners
39+
class PropertyListenerHelper<PropertyType: RivePropertyWithListeners> {
40+
private var listenerIds: [UUID] = []
41+
weak var property: PropertyType?
42+
43+
init(property: PropertyType) {
44+
self.property = property
45+
}
46+
47+
/// Adds a listener to the property and automatically tracks its ID for cleanup
48+
func addListener(_ callback: @escaping (PropertyType.ListenerValueType) -> Void) {
49+
guard let property = property else { return }
50+
let id = property.addListener(callback)
51+
listenerIds.append(id)
52+
}
53+
54+
func removeListeners() throws {
55+
guard let property = property else { return }
56+
for id in listenerIds {
57+
property.removeListener(id)
58+
}
59+
listenerIds.removeAll()
60+
}
61+
62+
func dispose() throws {
63+
try? removeListeners()
64+
}
65+
}
66+
67+
/// Protocol for properties that have typed values (Bool, String, Double, etc.)
68+
/// Provides a default addListener implementation
69+
protocol ValuedPropertyProtocol<ValueType> {
70+
associatedtype PropertyType: RivePropertyWithListeners
71+
associatedtype ValueType
72+
73+
var property: PropertyType! { get }
74+
var helper: PropertyListenerHelper<PropertyType> { get }
75+
76+
func addListener(onChanged: @escaping (ValueType) -> Void) throws
77+
func removeListeners() throws
78+
func dispose() throws
79+
}
80+
81+
/// Default implementations for lifecycle methods (always available)
82+
extension ValuedPropertyProtocol {
83+
func removeListeners() throws {
84+
try helper.removeListeners()
85+
}
86+
87+
func dispose() throws {
88+
try helper.dispose()
89+
}
90+
}
91+
92+
/// Automatic addListener() ONLY when ListenerValueType == ValueType (no conversion needed)
93+
extension ValuedPropertyProtocol where PropertyType.ListenerValueType == ValueType {
94+
func addListener(onChanged: @escaping (ValueType) -> Void) throws {
95+
helper.addListener(onChanged) // Types match, just forward directly!
96+
}
97+
}

ios/HybridViewModel.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,42 @@ import RiveRuntime
22

33
class HybridViewModel: HybridViewModelSpec {
44
let viewModel: RiveDataBindingViewModel?
5-
5+
66
init(viewModel: RiveDataBindingViewModel) {
77
self.viewModel = viewModel
88
}
9-
9+
1010
override init() {
1111
self.viewModel = nil
1212
super.init()
1313
}
14-
14+
1515
var propertyCount: Double { Double(viewModel?.propertyCount ?? 0) }
16-
16+
1717
var instanceCount: Double { Double(viewModel?.instanceCount ?? 0) }
18-
18+
1919
var modelName: String { viewModel?.name ?? "" }
20-
20+
2121
func createInstanceByIndex(index: Double) throws -> (any HybridViewModelInstanceSpec)? {
2222
guard let viewModel = viewModel,
2323
let vmi = viewModel.createInstance(fromIndex: UInt(index)) else { return nil }
2424
return HybridViewModelInstance(viewModelInstance: vmi)
2525
}
26-
26+
2727
func createInstanceByName(name: String) throws -> (any HybridViewModelInstanceSpec)? {
2828
guard let viewModel = viewModel,
2929
let vmi = viewModel.createInstance(fromName: name) else { return nil }
3030
return HybridViewModelInstance(viewModelInstance: vmi)
3131
}
32-
32+
3333
func createDefaultInstance() throws -> (any HybridViewModelInstanceSpec)? {
3434
guard let viewModel = viewModel,
3535
let vmi = viewModel.createDefaultInstance() else {
3636
return nil
3737
}
3838
return HybridViewModelInstance(viewModelInstance: vmi)
3939
}
40-
40+
4141
func createInstance() throws -> (any HybridViewModelInstanceSpec)? {
4242
guard let viewModel = viewModel,
4343
let vmi = viewModel.createInstance() else { return nil }
Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import NitroModules
22
import RiveRuntime
33

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

8-
init(property: RiveDataBindingViewModel.Instance.BooleanProperty) {
8+
init(property: BooleanPropertyType) {
99
self.property = property
1010
super.init()
1111
}
@@ -26,22 +26,4 @@ class HybridViewModelBooleanProperty: HybridViewModelBooleanPropertySpec {
2626
property.value = newValue
2727
}
2828
}
29-
30-
func addListener(onChanged: @escaping (Bool) -> Void) throws {
31-
let id = property.addListener({ value in
32-
onChanged(value)
33-
})
34-
listenerIds.append(id)
35-
}
36-
37-
func removeListeners() throws {
38-
for id in listenerIds {
39-
property.removeListener(id)
40-
}
41-
listenerIds.removeAll()
42-
}
43-
44-
func dispose() throws {
45-
try? removeListeners()
46-
}
4729
}

ios/HybridViewModelColorProperty.swift

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import NitroModules
22
import RiveRuntime
33

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

8-
init(property: RiveDataBindingViewModel.Instance.ColorProperty) {
8+
init(property: ColorPropertyType) {
99
self.property = property
1010
super.init()
1111
}
@@ -27,22 +27,11 @@ class HybridViewModelColorProperty: HybridViewModelColorPropertySpec {
2727
}
2828
}
2929

30+
// Custom addListener because we need to convert UIColor → Double
3031
func addListener(onChanged: @escaping (Double) -> Void) throws {
31-
let id = property.addListener({ value in
32-
onChanged(value.toHexDouble())
33-
})
34-
listenerIds.append(id)
35-
}
36-
37-
func removeListeners() throws {
38-
for id in listenerIds {
39-
property.removeListener(id)
32+
helper.addListener { (color: UIColor) in
33+
onChanged(color.toHexDouble())
4034
}
41-
listenerIds.removeAll()
42-
}
43-
44-
func dispose() throws {
45-
try? removeListeners()
4635
}
4736
}
4837

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import NitroModules
22
import RiveRuntime
33

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

8-
init(property: RiveDataBindingViewModel.Instance.EnumProperty) {
8+
init(property: EnumPropertyType) {
99
self.property = property
1010
super.init()
1111
}
@@ -26,22 +26,4 @@ class HybridViewModelEnumProperty: HybridViewModelEnumPropertySpec {
2626
property.value = newValue
2727
}
2828
}
29-
30-
func addListener(onChanged: @escaping (String) -> Void) throws {
31-
let id = property.addListener({ value in
32-
onChanged(value)
33-
})
34-
listenerIds.append(id)
35-
}
36-
37-
func removeListeners() throws {
38-
for id in listenerIds {
39-
property.removeListener(id)
40-
}
41-
listenerIds.removeAll()
42-
}
43-
44-
func dispose() throws {
45-
try? removeListeners()
46-
}
4729
}

ios/HybridViewModelInstance.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,43 @@ import RiveRuntime
22

33
class HybridViewModelInstance: HybridViewModelInstanceSpec {
44
let viewModelInstance: RiveDataBindingViewModel.Instance?
5-
5+
66
init(viewModelInstance: RiveDataBindingViewModel.Instance) {
77
self.viewModelInstance = viewModelInstance
88
}
9-
9+
1010
override init() {
1111
self.viewModelInstance = nil
1212
super.init()
1313
}
14-
14+
1515
var instanceName: String { viewModelInstance?.name ?? "" }
16-
16+
1717
func numberProperty(path: String) throws -> (any HybridViewModelNumberPropertySpec)? {
1818
guard let property = viewModelInstance?.numberProperty(fromPath: path) else { return nil }
1919
return HybridViewModelNumberProperty(property: property)
2020
}
21-
21+
2222
func stringProperty(path: String) throws -> (any HybridViewModelStringPropertySpec)? {
2323
guard let property = viewModelInstance?.stringProperty(fromPath: path) else { return nil }
2424
return HybridViewModelStringProperty(property: property)
2525
}
26-
26+
2727
func booleanProperty(path: String) throws -> (any HybridViewModelBooleanPropertySpec)? {
2828
guard let property = viewModelInstance?.booleanProperty(fromPath: path) else { return nil }
2929
return HybridViewModelBooleanProperty(property: property)
3030
}
31-
31+
3232
func colorProperty(path: String) throws -> (any HybridViewModelColorPropertySpec)? {
3333
guard let property = viewModelInstance?.colorProperty(fromPath: path) else { return nil }
3434
return HybridViewModelColorProperty(property: property)
3535
}
36-
36+
3737
func enumProperty(path: String) throws -> (any HybridViewModelEnumPropertySpec)? {
3838
guard let property = viewModelInstance?.enumProperty(fromPath: path) else { return nil }
3939
return HybridViewModelEnumProperty(property: property)
4040
}
41-
41+
4242
func triggerProperty(path: String) throws -> (any HybridViewModelTriggerPropertySpec)? {
4343
guard let property = viewModelInstance?.triggerProperty(fromPath: path) else { return nil }
4444
return HybridViewModelTriggerProperty(property: property)
Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import RiveRuntime
22

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

7-
init(property: RiveDataBindingViewModel.Instance.NumberProperty) {
7+
init(property: NumberPropertyType) {
88
self.property = property
99
super.init()
1010
}
@@ -26,23 +26,10 @@ class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec {
2626
}
2727
}
2828

29+
// Custom addListener needed because ListenerValueType (Float) != ValueType (Double)
2930
func addListener(onChanged: @escaping (Double) -> Void) throws {
30-
let id = property.addListener({ value in
31+
helper.addListener { (value: Float) in
3132
onChanged(Double(value))
32-
})
33-
34-
listenerIds.append(id)
35-
36-
}
37-
38-
func removeListeners() throws {
39-
for id in listenerIds {
40-
property.removeListener(id)
4133
}
42-
listenerIds.removeAll()
43-
}
44-
45-
func dispose() throws {
46-
try? removeListeners()
4734
}
4835
}

0 commit comments

Comments
 (0)