Skip to content

Commit 40c3275

Browse files
committed
Update ObservationRegistrar logic and implement usage in ObservableViewModelPublisher
1 parent a107571 commit 40c3275

File tree

10 files changed

+193
-101
lines changed

10 files changed

+193
-101
lines changed

KMMViewModel.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
1D198B312933C01800EF778D /* KMMViewModelCoreObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D198B2F2933C01800EF778D /* KMMViewModelCoreObjC.m */; };
1919
1D198B322933C04400EF778D /* KMMViewModelCoreObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D198B272933BFD900EF778D /* KMMViewModelCoreObjC.framework */; };
2020
1D2AAC592BB1F528005F1344 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2AAC582BB1F528005F1344 /* Observable.swift */; };
21-
1D2AAC5B2BB2053D005F1344 /* ViewModelObservationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2AAC5A2BB2053D005F1344 /* ViewModelObservationRegistrar.swift */; };
21+
1D2AAC5B2BB2053D005F1344 /* ObservationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2AAC5A2BB2053D005F1344 /* ObservationRegistrar.swift */; };
2222
1D2AAC5D2BB205AC005F1344 /* ObservableProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2AAC5C2BB205AC005F1344 /* ObservableProperty.swift */; };
2323
1D43F3EC2ABAFCA600EB3DFE /* ObservableViewModelPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43F3EB2ABAFCA600EB3DFE /* ObservableViewModelPublisher.swift */; };
2424
1D43F3EE2ABAFD7D00EB3DFE /* ObservableViewModelPublishers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43F3ED2ABAFD7D00EB3DFE /* ObservableViewModelPublishers.swift */; };
@@ -56,7 +56,7 @@
5656
1D198B2E2933C01800EF778D /* KMMVMViewModelScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMMVMViewModelScope.h; sourceTree = "<group>"; };
5757
1D198B2F2933C01800EF778D /* KMMViewModelCoreObjC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMMViewModelCoreObjC.m; sourceTree = "<group>"; };
5858
1D2AAC582BB1F528005F1344 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
59-
1D2AAC5A2BB2053D005F1344 /* ViewModelObservationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelObservationRegistrar.swift; sourceTree = "<group>"; };
59+
1D2AAC5A2BB2053D005F1344 /* ObservationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationRegistrar.swift; sourceTree = "<group>"; };
6060
1D2AAC5C2BB205AC005F1344 /* ObservableProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableProperty.swift; sourceTree = "<group>"; };
6161
1D43F3EB2ABAFCA600EB3DFE /* ObservableViewModelPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableViewModelPublisher.swift; sourceTree = "<group>"; };
6262
1D43F3ED2ABAFD7D00EB3DFE /* ObservableViewModelPublishers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableViewModelPublishers.swift; sourceTree = "<group>"; };
@@ -117,12 +117,12 @@
117117
children = (
118118
1D6641DC2A5175C3000180D7 /* ChildViewModels.swift */,
119119
1DDAF21D293545DD0049C114 /* KMMViewModel.swift */,
120+
1D2AAC582BB1F528005F1344 /* Observable.swift */,
121+
1D2AAC5C2BB205AC005F1344 /* ObservableProperty.swift */,
120122
1D0DA80129336AD40057DDAD /* ObservableViewModel.swift */,
121123
1D43F3EB2ABAFCA600EB3DFE /* ObservableViewModelPublisher.swift */,
122124
1D43F3ED2ABAFD7D00EB3DFE /* ObservableViewModelPublishers.swift */,
123-
1D2AAC582BB1F528005F1344 /* Observable.swift */,
124-
1D2AAC5A2BB2053D005F1344 /* ViewModelObservationRegistrar.swift */,
125-
1D2AAC5C2BB205AC005F1344 /* ObservableProperty.swift */,
125+
1D2AAC5A2BB2053D005F1344 /* ObservationRegistrar.swift */,
126126
);
127127
path = KMMViewModelCore;
128128
sourceTree = "<group>";
@@ -312,7 +312,7 @@
312312
buildActionMask = 2147483647;
313313
files = (
314314
1D43F3EE2ABAFD7D00EB3DFE /* ObservableViewModelPublishers.swift in Sources */,
315-
1D2AAC5B2BB2053D005F1344 /* ViewModelObservationRegistrar.swift in Sources */,
315+
1D2AAC5B2BB2053D005F1344 /* ObservationRegistrar.swift in Sources */,
316316
1D43F3EC2ABAFCA600EB3DFE /* ObservableViewModelPublisher.swift in Sources */,
317317
1DDAF21E293545DD0049C114 /* KMMViewModel.swift in Sources */,
318318
1D6641DD2A5175C3000180D7 /* ChildViewModels.swift in Sources */,

KMMViewModelCore/Observable.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import Observation
1010
import KMMViewModelCoreObjC
1111

1212
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
13-
public protocol Observable: Observation.Observable {
14-
associatedtype ViewModel: Observable
15-
16-
@ViewModelObservationRegistrarBuilder<ViewModel>
17-
var viewModelObservationRegistrar: ViewModelObservationRegistrar<ViewModel> { get }
13+
public protocol Observable: KMMViewModel, Observation.Observable {
14+
@ObservationRegistrarBuilder<Self>
15+
var observationRegistrar: ObservationRegistrar { get }
1816
}

KMMViewModelCore/ObservableProperty.swift

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,40 @@
88
import Observation
99

1010
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
11-
public protocol ObservableProperty<ViewModel> {
12-
associatedtype ViewModel: Observable
11+
public protocol ObservableProperty {
1312

14-
func willSet(registrar: ObservationRegistrar, viewModel: ViewModel)
13+
func initialize(subject: any Observable)
1514

16-
func didSet(registrar: ObservationRegistrar, viewModel: ViewModel)
15+
func willSet(registrar: Observation.ObservationRegistrar, subject: any Observable)
1716

18-
func access(registrar: ObservationRegistrar, viewModel: ViewModel)
17+
func didSet(registrar: Observation.ObservationRegistrar, subject: any Observable)
18+
19+
func access(registrar: Observation.ObservationRegistrar, subject: any Observable)
1920
}
2021

2122
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
2223
extension KeyPath: ObservableProperty where Root: Observable {
23-
public typealias ViewModel = Root
2424

25-
public func willSet(registrar: ObservationRegistrar, viewModel: ViewModel) {
26-
registrar.willSet(viewModel, keyPath: self)
25+
private func root(_ subject: any Observable) -> Root {
26+
guard let root = subject as? Root else {
27+
fatalError("subject must be of type Root")
28+
}
29+
return root
30+
}
31+
32+
public func initialize(subject: any Observable) {
33+
_ = root(subject)[keyPath: self]
34+
}
35+
36+
public func willSet(registrar: Observation.ObservationRegistrar, subject: any Observable) {
37+
registrar.willSet(root(subject), keyPath: self)
2738
}
2839

29-
public func didSet(registrar: ObservationRegistrar, viewModel: ViewModel) {
30-
registrar.didSet(viewModel, keyPath: self)
40+
public func didSet(registrar: Observation.ObservationRegistrar, subject: any Observable) {
41+
registrar.didSet(root(subject), keyPath: self)
3142
}
3243

33-
public func access(registrar: ObservationRegistrar, viewModel: ViewModel) {
34-
registrar.access(viewModel, keyPath: self)
44+
public func access(registrar: Observation.ObservationRegistrar, subject: any Observable) {
45+
registrar.access(root(subject), keyPath: self)
3546
}
3647
}

KMMViewModelCore/ObservableViewModelPublisher.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ public final class ObservableViewModelPublisher: Publisher {
2020

2121
internal init(_ viewModel: any KMMViewModel, _ objectWillChange: ObservableObjectPublisher) {
2222
self.viewModel = viewModel
23-
viewModel.viewModelScope.setSendObjectWillChange { [weak self] in
24-
self?.publisher.send()
23+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *), let observable = viewModel as? (any Observable) {
24+
observable.observationRegistrar.initialize(observable, publisher)
25+
} else {
26+
viewModel.viewModelScope.setPropertyWillSet { [weak self] _ in
27+
self?.publisher.send()
28+
}
2529
}
2630
objectWillChangeCancellable = objectWillChange.sink { [weak self] _ in
2731
self?.publisher.send()
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// ObservationRegistrar.swift
3+
// KMMViewModelCore
4+
//
5+
// Created by Rick Clephas on 25/03/2024.
6+
//
7+
8+
import Foundation
9+
import Observation
10+
import Combine
11+
12+
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
13+
public class ObservationRegistrar {
14+
15+
private let registrar = Observation.ObservationRegistrar()
16+
17+
private let observableProperties: [ObservableProperty]
18+
private var properties: [NSObject:[ObservableProperty]] = [:]
19+
20+
internal init(_ observableProperties: [ObservableProperty]) {
21+
self.observableProperties = observableProperties
22+
}
23+
24+
private weak var observable: (any Observable)? = nil
25+
private var publisher: ObservableObjectPublisher? = nil
26+
27+
internal func initialize(_ observable: any Observable, _ publisher: ObservableObjectPublisher) {
28+
observable.viewModelScope.setPropertyAccess(access)
29+
observable.viewModelScope.setPropertyWillSet(willSet)
30+
observable.viewModelScope.setPropertyDidSet(didSet)
31+
for observableProperty in observableProperties {
32+
Thread.current.threadDictionary["observableProperty"] = observableProperty
33+
observableProperty.initialize(subject: observable)
34+
}
35+
Thread.current.threadDictionary.removeObject(forKey: "observableProperty")
36+
self.observable = observable
37+
self.publisher = publisher
38+
}
39+
40+
private func access(_ property: NSObject) {
41+
guard let observable else {
42+
if let observableProperty = Thread.current.threadDictionary["observableProperty"] as? ObservableProperty {
43+
var observableProperties = properties[property] ?? []
44+
observableProperties.append(observableProperty)
45+
properties[property] = observableProperties
46+
}
47+
return
48+
}
49+
guard let properties = properties[property] else { return }
50+
for property in properties {
51+
property.access(registrar: registrar, subject: observable)
52+
}
53+
}
54+
55+
private func willSet(_ property: NSObject) {
56+
guard let observable, let properties = properties[property] else {
57+
publisher?.send()
58+
return
59+
}
60+
for property in properties {
61+
property.willSet(registrar: registrar, subject: observable)
62+
}
63+
}
64+
65+
private func didSet(_ property: NSObject) {
66+
guard let observable, let properties = properties[property] else { return }
67+
for property in properties {
68+
property.didSet(registrar: registrar, subject: observable)
69+
}
70+
}
71+
}
72+
73+
@resultBuilder
74+
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
75+
public struct ObservationRegistrarBuilder<ViewModel: Observable> {
76+
77+
public static func buildExpression<Member>(_ expression: KeyPath<ViewModel, Member>) -> ObservableProperty {
78+
return expression
79+
}
80+
81+
public static func buildBlock(_ components: ObservableProperty...) -> ObservationRegistrar {
82+
return ObservationRegistrar(components)
83+
}
84+
}

KMMViewModelCore/ViewModelObservationRegistrar.swift

Lines changed: 0 additions & 57 deletions
This file was deleted.

KMMViewModelCoreObjC/KMMVMViewModelScope.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ __attribute__((swift_name("ViewModelScope")))
1414
@protocol KMMVMViewModelScope
1515
- (void)increaseSubscriptionCount;
1616
- (void)decreaseSubscriptionCount;
17-
- (void)setSendObjectWillChange:(void (^ _Nonnull)(void))sendObjectWillChange;
17+
- (void)setPropertyAccess:(void (^ _Nonnull)(NSObject * _Nonnull))propertyAccess;
18+
- (void)setPropertyWillSet:(void (^ _Nonnull)(NSObject * _Nonnull))propertyWillSet;
19+
- (void)setPropertyDidSet:(void (^ _Nonnull)(NSObject * _Nonnull))propertyDidSet;
1820
- (void)cancel;
1921
@end
2022

kmm-viewmodel-core/src/appleMain/kotlin/com/rickclephas/kmm/viewmodel/StateFlow.kt

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public actual fun <T> MutableStateFlow(
1414
): MutableStateFlow<T> = MutableStateFlowImpl(viewModelScope.asImpl(), MutableStateFlow(value))
1515

1616
/**
17-
* A [MutableStateFlow] that triggers [ViewModelScopeImpl.sendObjectWillChange]
17+
* A [MutableStateFlow] that triggers [ViewModelScopeImpl.propertyWillSet],
18+
* [ViewModelScopeImpl.propertyDidSet] and [ViewModelScopeImpl.propertyAccess]
1819
* and accounts for the [ViewModelScopeImpl.subscriptionCount].
1920
*/
2021
private class MutableStateFlowImpl<T>(
@@ -23,16 +24,21 @@ private class MutableStateFlowImpl<T>(
2324
): MutableStateFlow<T> {
2425

2526
override var value: T
26-
get() = stateFlow.value
27+
get() {
28+
viewModelScope.propertyAccess(this)
29+
return stateFlow.value
30+
}
2731
set(value) {
28-
if (stateFlow.value != value) {
29-
viewModelScope.sendObjectWillChange()
30-
}
32+
val changed = stateFlow.value != value
33+
if (changed) viewModelScope.propertyWillSet(this)
3134
stateFlow.value = value
35+
if (changed) viewModelScope.propertyDidSet(this)
3236
}
3337

34-
override val replayCache: List<T>
35-
get() = stateFlow.replayCache
38+
override val replayCache: List<T> get() {
39+
viewModelScope.propertyAccess(this)
40+
return stateFlow.replayCache
41+
}
3642

3743
override val subscriptionCount: StateFlow<Int> =
3844
SubscriptionCountFlow(viewModelScope.subscriptionCount, stateFlow.subscriptionCount)
@@ -41,10 +47,11 @@ private class MutableStateFlowImpl<T>(
4147
stateFlow.collect(collector)
4248

4349
override fun compareAndSet(expect: T, update: T): Boolean {
44-
if (stateFlow.value == expect && expect != update) {
45-
viewModelScope.sendObjectWillChange()
46-
}
47-
return stateFlow.compareAndSet(expect, update)
50+
val changed = stateFlow.value == expect && expect != update
51+
if (changed) viewModelScope.propertyWillSet(this)
52+
val result = stateFlow.compareAndSet(expect, update)
53+
if (changed) viewModelScope.propertyDidSet(this)
54+
return result
4855
}
4956

5057
@ExperimentalCoroutinesApi

kmm-viewmodel-core/src/appleMain/kotlin/com/rickclephas/kmm/viewmodel/ViewModelScope.kt

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,52 @@ public class ViewModelScopeImpl internal constructor(
5959
_subscriptionCount.update { it - 1 }
6060
}
6161

62-
private var sendObjectWillChange: (() -> Unit)? = null
62+
private var propertyAccess: ((NSObject) -> Unit)? = null
6363

64-
override fun setSendObjectWillChange(sendObjectWillChange: () -> Unit) {
65-
if (this.sendObjectWillChange != null) {
64+
override fun setPropertyAccess(propertyAccess: (NSObject?) -> Unit) {
65+
if (this.propertyAccess != null) {
6666
throw IllegalStateException("KMMViewModel can't be wrapped more than once")
6767
}
68-
this.sendObjectWillChange = sendObjectWillChange
68+
this.propertyAccess = propertyAccess
6969
}
7070

7171
/**
72-
* Invokes the object will change listener set by [setSendObjectWillChange].
72+
* Invokes the listener set by [setPropertyAccess].
7373
*/
74-
public fun sendObjectWillChange() {
75-
sendObjectWillChange?.invoke()
74+
public fun propertyAccess(property: Any) {
75+
propertyAccess?.invoke(property as NSObject)
76+
}
77+
78+
private var propertyWillSet: ((NSObject) -> Unit)? = null
79+
80+
override fun setPropertyWillSet(propertyWillSet: (NSObject?) -> Unit) {
81+
if (this.propertyWillSet != null) {
82+
throw IllegalStateException("KMMViewModel can't be wrapped more than once")
83+
}
84+
this.propertyWillSet = propertyWillSet
85+
}
86+
87+
/**
88+
* Invokes the listener set by [setPropertyWillSet].
89+
*/
90+
public fun propertyWillSet(property: Any) {
91+
propertyWillSet?.invoke(property as NSObject)
92+
}
93+
94+
private var propertyDidSet: ((NSObject) -> Unit)? = null
95+
96+
override fun setPropertyDidSet(propertyDidSet: (NSObject?) -> Unit) {
97+
if (this.propertyDidSet != null) {
98+
throw IllegalStateException("KMMViewModel can't be wrapped more than once")
99+
}
100+
this.propertyDidSet = propertyDidSet
101+
}
102+
103+
/**
104+
* Invokes the listener set by [setPropertyDidSet].
105+
*/
106+
public fun propertyDidSet(property: Any) {
107+
propertyDidSet?.invoke(property as NSObject)
76108
}
77109

78110
override fun cancel() {

0 commit comments

Comments
 (0)