Skip to content

Commit df51538

Browse files
committed
Merge branch 'master' into feature/kotlin-2.3.0
2 parents 9bee31b + f6f15e9 commit df51538

File tree

43 files changed

+839
-398
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+839
-398
lines changed

KMPObservableViewModelCore.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'KMPObservableViewModelCore'
3-
s.version = '1.0.0-BETA-14'
3+
s.version = '1.0.0'
44
s.summary = 'Library to share Kotlin ViewModels with Swift'
55

66
s.homepage = 'https://github.com/rickclephas/KMP-ObservableViewModel'

KMPObservableViewModelCore/ChildViewModels.swift

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,11 @@ import KMPObservableViewModelCoreObjC
99

1010
public extension ViewModel {
1111

12-
private func setChildViewModelPublishers(_ keyPath: AnyKeyPath, _ publishers: AnyHashable?) {
13-
if let publishers = publishers {
14-
observableViewModelPublishers(for: self).childPublishers[keyPath] = publishers
15-
} else {
16-
observableViewModelPublishers(for: self).childPublishers.removeValue(forKey: keyPath)
17-
}
18-
}
19-
2012
private func setChildViewModel<VM: ViewModel>(
2113
_ viewModel: VM?,
2214
at keyPath: AnyKeyPath
2315
) {
24-
setChildViewModelPublishers(keyPath, observableViewModelPublishers(for: viewModel))
16+
viewModelWillChange.cancellable.setChildCancellables(keyPath, ViewModelCancellable.get(for: viewModel))
2517
}
2618

2719
/// Stores a reference to the `ObservableObject` for the specified child `ViewModel`.
@@ -48,8 +40,8 @@ public extension ViewModel {
4840
_ viewModels: [VM?]?,
4941
at keyPath: AnyKeyPath
5042
) {
51-
setChildViewModelPublishers(keyPath, viewModels?.map { viewModel in
52-
observableViewModelPublishers(for: viewModel)
43+
viewModelWillChange.cancellable.setChildCancellables(keyPath, viewModels?.map { viewModel in
44+
ViewModelCancellable.get(for: viewModel)
5345
})
5446
}
5547

@@ -95,8 +87,8 @@ public extension ViewModel {
9587
_ viewModels: Set<VM?>?,
9688
at keyPath: AnyKeyPath
9789
) {
98-
setChildViewModelPublishers(keyPath, viewModels?.map { viewModel in
99-
observableViewModelPublishers(for: viewModel)
90+
viewModelWillChange.cancellable.setChildCancellables(keyPath, viewModels?.map { viewModel in
91+
ViewModelCancellable.get(for: viewModel)
10092
})
10193
}
10294

@@ -142,8 +134,8 @@ public extension ViewModel {
142134
_ viewModels: [Key : VM?]?,
143135
at keyPath: AnyKeyPath
144136
) {
145-
setChildViewModelPublishers(keyPath, viewModels?.mapValues { viewModel in
146-
observableViewModelPublishers(for: viewModel)
137+
viewModelWillChange.cancellable.setChildCancellables(keyPath, viewModels?.mapValues { viewModel in
138+
ViewModelCancellable.get(for: viewModel)
147139
})
148140
}
149141

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// ObservableProperties.swift
3+
// KMPObservableViewModelCore
4+
//
5+
// Created by Rick Clephas on 25/10/2025.
6+
//
7+
8+
import Observation
9+
import KMPObservableViewModelCoreObjC
10+
11+
/// Helper object that maps any external `Property` to an `ObservableProperty`.
12+
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
13+
internal final class ObservableProperties {
14+
15+
private var properties: [ObjectIdentifier:ObservableProperty] = [:]
16+
17+
private func observableProperty(_ property: any Property) -> ObservableProperty {
18+
let identifier = ObjectIdentifier(property)
19+
if let observableProperty = properties[identifier] { return observableProperty }
20+
let observableProperty = ObservableProperty(property)
21+
properties[identifier] = observableProperty
22+
return observableProperty
23+
}
24+
25+
func access(_ property: any Property) {
26+
observableProperty(property).access()
27+
}
28+
29+
func willSet(_ property: any Property) {
30+
observableProperty(property).willSet()
31+
}
32+
33+
func didSet(_ property: any Property) {
34+
observableProperty(property).didSet()
35+
}
36+
}
37+
38+
/// Helper object that turns an external `Property` into a Swift observable property.
39+
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
40+
private final class ObservableProperty: Observable {
41+
42+
private let registrar = ObservationRegistrar()
43+
private let property: any Property
44+
45+
init(_ property: any Property) {
46+
self.property = property
47+
}
48+
49+
var value: Any? { property.value }
50+
51+
func access() {
52+
registrar.access(self, keyPath: \.value)
53+
}
54+
55+
func willSet() {
56+
registrar.willSet(self, keyPath: \.value)
57+
}
58+
59+
func didSet() {
60+
registrar.didSet(self, keyPath: \.value)
61+
}
62+
}

KMPObservableViewModelCore/ObservableViewModel.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ import KMPObservableViewModelCoreObjC
1313
public func observableViewModel<VM: ViewModel>(
1414
for viewModel: VM
1515
) -> ObservableViewModel<VM> {
16-
let publishers = observableViewModelPublishers(for: viewModel)
17-
return ObservableViewModel(publishers, viewModel)
16+
return ObservableViewModel(viewModel)
1817
}
1918

2019
/// Gets an `ObservableObject` for the specified `ViewModel`.
@@ -35,13 +34,13 @@ public final class ObservableViewModel<VM: ViewModel>: ObservableObject, Hashabl
3534
/// The observed `ViewModel`.
3635
public let viewModel: VM
3736

38-
/// Holds a strong reference to the publishers
39-
private let publishers: ObservableViewModelPublishers
37+
/// Holds a strong reference to the cancellable
38+
private let cancellable: AnyCancellable
4039

41-
internal init(_ publishers: ObservableViewModelPublishers, _ viewModel: VM) {
42-
objectWillChange = publishers.publisher
40+
internal init(_ viewModel: VM) {
41+
objectWillChange = viewModel.viewModelWillChange
4342
self.viewModel = viewModel
44-
self.publishers = publishers
43+
cancellable = ViewModelCancellable.get(for: viewModel)
4544
}
4645

4746
public static func == (lhs: ObservableViewModel<VM>, rhs: ObservableViewModel<VM>) -> Bool {

KMPObservableViewModelCore/ObservableViewModelPublisher.swift

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,62 @@ import Combine
99
import KMPObservableViewModelCoreObjC
1010

1111
/// Publisher for `ObservableViewModel` that connects to the `ViewModelScope`.
12-
public final class ObservableViewModelPublisher: Publisher {
12+
public final class ObservableViewModelPublisher: Combine.Publisher, KMPObservableViewModelCoreObjC.Publisher {
1313
public typealias Output = Void
1414
public typealias Failure = Never
1515

16-
internal weak var viewModel: (any ViewModel)?
16+
internal let cancellable = ViewModelCancellable()
1717

18-
private let publisher = ObservableObjectPublisher()
19-
private var objectWillChangeCancellable: AnyCancellable? = nil
18+
private let publisher: ObservableObjectPublisher
19+
private let subscriptionCount: any SubscriptionCount
2020

21-
internal init(_ viewModel: any ViewModel, _ objectWillChange: ObservableObjectPublisher) {
22-
self.viewModel = viewModel
23-
viewModel.viewModelScope.setSendObjectWillChange { [weak self] in
24-
self?.publisher.send()
25-
}
26-
objectWillChangeCancellable = objectWillChange.sink { [weak self] _ in
27-
self?.publisher.send()
21+
private var _observableProperties: Any? = nil
22+
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
23+
private var observableProperties: ObservableProperties? {
24+
_observableProperties as! ObservableProperties?
25+
}
26+
27+
internal init(_ viewModel: any ViewModel) {
28+
self.publisher = viewModel.objectWillChange
29+
self.subscriptionCount = viewModel.viewModelScope.subscriptionCount
30+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *), viewModel is Observable {
31+
_observableProperties = ObservableProperties()
2832
}
2933
}
3034

3135
public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Void == S.Input {
32-
viewModel?.viewModelScope.increaseSubscriptionCount()
33-
publisher.receive(subscriber: ObservableViewModelSubscriber(self, subscriber))
36+
subscriptionCount.increase()
37+
publisher.receive(subscriber: ObservableViewModelSubscriber(subscriptionCount, subscriber))
38+
}
39+
40+
public func access(_ property: any Property) {
41+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *), let observableProperties {
42+
observableProperties.access(property)
43+
}
44+
}
45+
46+
public func willSet(_ property: any Property) {
47+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *), let observableProperties {
48+
observableProperties.willSet(property)
49+
} else {
50+
publisher.send()
51+
}
3452
}
3553

36-
deinit {
37-
guard let viewModel else { return }
38-
if let cancellable = viewModel as? Cancellable {
39-
cancellable.cancel()
54+
public func didSet(_ property: any Property) {
55+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *), let observableProperties {
56+
observableProperties.didSet(property)
4057
}
41-
viewModel.clear()
58+
}
59+
}
60+
61+
internal extension KMPObservableViewModelCoreObjC.Publisher {
62+
/// Casts this `Publisher` to an `ObservableViewModelPublisher`.
63+
func cast() -> ObservableViewModelPublisher {
64+
guard let publisher = self as? ObservableViewModelPublisher else {
65+
fatalError("Publisher must be an ObservableViewModelPublisher")
66+
}
67+
return publisher
4268
}
4369
}
4470

@@ -47,16 +73,16 @@ private class ObservableViewModelSubscriber<S>: Subscriber where S : Subscriber,
4773
typealias Input = Void
4874
typealias Failure = Never
4975

50-
private let publisher: ObservableViewModelPublisher
76+
private let subscriptionCount: any SubscriptionCount
5177
private let subscriber: S
5278

53-
init(_ publisher: ObservableViewModelPublisher, _ subscriber: S) {
54-
self.publisher = publisher
79+
init(_ subscriptionCount: any SubscriptionCount, _ subscriber: S) {
80+
self.subscriptionCount = subscriptionCount
5581
self.subscriber = subscriber
5682
}
5783

5884
func receive(subscription: Subscription) {
59-
subscriber.receive(subscription: ObservableViewModelSubscription(publisher, subscription))
85+
subscriber.receive(subscription: ObservableViewModelSubscription(subscriptionCount, subscription))
6086
}
6187

6288
func receive(_ input: Void) -> Subscribers.Demand {
@@ -71,24 +97,21 @@ private class ObservableViewModelSubscriber<S>: Subscriber where S : Subscriber,
7197
/// Subscription for `ObservableViewModelPublisher` that decreases the subscription count upon cancellation.
7298
private class ObservableViewModelSubscription: Subscription {
7399

74-
private let publisher: ObservableViewModelPublisher
100+
private var subscriptionCount: (any SubscriptionCount)?
75101
private let subscription: Subscription
76102

77-
init(_ publisher: ObservableViewModelPublisher, _ subscription: Subscription) {
78-
self.publisher = publisher
103+
init(_ subscriptionCount: any SubscriptionCount, _ subscription: Subscription) {
104+
self.subscriptionCount = subscriptionCount
79105
self.subscription = subscription
80106
}
81107

82108
func request(_ demand: Subscribers.Demand) {
83109
subscription.request(demand)
84110
}
85111

86-
private var cancelled = false
87-
88112
func cancel() {
89113
subscription.cancel()
90-
guard !cancelled else { return }
91-
cancelled = true
92-
publisher.viewModel?.viewModelScope.decreaseSubscriptionCount()
114+
subscriptionCount?.decrease()
115+
subscriptionCount = nil
93116
}
94117
}

KMPObservableViewModelCore/ObservableViewModelPublishers.swift

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

KMPObservableViewModelCore/ViewModel.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,24 @@
88
import Combine
99
import KMPObservableViewModelCoreObjC
1010

11-
/// A Kotlin Multiplatform Mobile ViewModel.
11+
/// A Kotlin Multiplatform ViewModel.
1212
public protocol ViewModel: ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
1313
/// The `ViewModelScope` of this `ViewModel`.
1414
var viewModelScope: ViewModelScope { get }
15+
/// An `ObservableViewModelPublisher` that emits before this `ViewModel` has changed.
16+
var viewModelWillChange: ObservableViewModelPublisher { get }
1517
/// Internal KMP-ObservableViewModel function used to clear the ViewModel.
1618
/// - Warning: You should NOT call this yourself!
1719
func clear()
1820
}
21+
22+
public extension ViewModel {
23+
var viewModelWillChange: ObservableViewModelPublisher {
24+
if let publisher = viewModelScope.publisher {
25+
return publisher.cast()
26+
}
27+
let publisher = ObservableViewModelPublisher(self)
28+
viewModelScope.publisher = publisher
29+
return publisher
30+
}
31+
}

0 commit comments

Comments
 (0)