Skip to content

Commit ecdc5e0

Browse files
authored
Merge pull request #94 from rickclephas/feature/v1-api
V1 API
2 parents 057a34c + 8353e0e commit ecdc5e0

File tree

17 files changed

+318
-263
lines changed

17 files changed

+318
-263
lines changed

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

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: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,27 @@ 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()
28-
}
21+
internal init(_ viewModel: any ViewModel) {
22+
self.publisher = viewModel.objectWillChange
23+
self.subscriptionCount = viewModel.viewModelScope.subscriptionCount
2924
}
3025

3126
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))
27+
subscriptionCount.increase()
28+
publisher.receive(subscriber: ObservableViewModelSubscriber(subscriptionCount, subscriber))
3429
}
3530

36-
deinit {
37-
guard let viewModel else { return }
38-
if let cancellable = viewModel as? Cancellable {
39-
cancellable.cancel()
40-
}
41-
viewModel.clear()
31+
public func send() {
32+
publisher.send()
4233
}
4334
}
4435

@@ -47,16 +38,16 @@ private class ObservableViewModelSubscriber<S>: Subscriber where S : Subscriber,
4738
typealias Input = Void
4839
typealias Failure = Never
4940

50-
private let publisher: ObservableViewModelPublisher
41+
private let subscriptionCount: any SubscriptionCount
5142
private let subscriber: S
5243

53-
init(_ publisher: ObservableViewModelPublisher, _ subscriber: S) {
54-
self.publisher = publisher
44+
init(_ subscriptionCount: any SubscriptionCount, _ subscriber: S) {
45+
self.subscriptionCount = subscriptionCount
5546
self.subscriber = subscriber
5647
}
5748

5849
func receive(subscription: Subscription) {
59-
subscriber.receive(subscription: ObservableViewModelSubscription(publisher, subscription))
50+
subscriber.receive(subscription: ObservableViewModelSubscription(subscriptionCount, subscription))
6051
}
6152

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

74-
private let publisher: ObservableViewModelPublisher
65+
private var subscriptionCount: (any SubscriptionCount)?
7566
private let subscription: Subscription
7667

77-
init(_ publisher: ObservableViewModelPublisher, _ subscription: Subscription) {
78-
self.publisher = publisher
68+
init(_ subscriptionCount: any SubscriptionCount, _ subscription: Subscription) {
69+
self.subscriptionCount = subscriptionCount
7970
self.subscription = subscription
8071
}
8172

8273
func request(_ demand: Subscribers.Demand) {
8374
subscription.request(demand)
8475
}
8576

86-
private var cancelled = false
87-
8877
func cancel() {
8978
subscription.cancel()
90-
guard !cancelled else { return }
91-
cancelled = true
92-
publisher.viewModel?.viewModelScope.decreaseSubscriptionCount()
79+
subscriptionCount?.decrease()
80+
subscriptionCount = nil
9381
}
9482
}

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 as! ObservableViewModelPublisher
26+
}
27+
let publisher = ObservableViewModelPublisher(self)
28+
viewModelScope.publisher = publisher
29+
return publisher
30+
}
31+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// ViewModelCancellable.swift
3+
// KMPObservableViewModelCore
4+
//
5+
// Created by Rick Clephas on 09/06/2025.
6+
//
7+
8+
import Combine
9+
10+
/// Helper object that provides a weakly cached `Cancellable` for a `ViewModel`.
11+
internal class ViewModelCancellable {
12+
13+
private var didInit = false
14+
private weak var cancellable: AnyCancellable?
15+
private var childCancellables: Dictionary<AnyKeyPath, AnyHashable>? = [:]
16+
17+
private func get(_ viewModel: any ViewModel) -> AnyCancellable {
18+
guard didInit else {
19+
let cancellable = AnyCancellable {
20+
if let cancellable = viewModel as? Cancellable {
21+
cancellable.cancel()
22+
}
23+
self.childCancellables = nil
24+
viewModel.clear()
25+
}
26+
self.cancellable = cancellable
27+
didInit = true
28+
return cancellable
29+
}
30+
guard let cancellable = self.cancellable else {
31+
fatalError("ObservableViewModel for \(viewModel) has been deallocated")
32+
}
33+
return cancellable
34+
}
35+
36+
func setChildCancellables(_ keyPath: AnyKeyPath, _ cancellables: AnyHashable?) {
37+
if let cancellables = cancellables {
38+
childCancellables?[keyPath] = cancellables
39+
} else {
40+
childCancellables?.removeValue(forKey: keyPath)
41+
}
42+
}
43+
44+
static func get(for viewModel: any ViewModel) -> AnyCancellable {
45+
viewModel.viewModelWillChange.cancellable.get(viewModel)
46+
}
47+
48+
static func get(for viewModel: (any ViewModel)?) -> AnyCancellable? {
49+
guard let viewModel else { return nil }
50+
let cancellable = get(for: viewModel)
51+
return cancellable
52+
}
53+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// KMPOVMPublisher.h
3+
// KMPObservableViewModelCoreObjC
4+
//
5+
// Created by Rick Clephas on 09/06/2025.
6+
//
7+
8+
#ifndef KMPOVMPublisher_h
9+
#define KMPOVMPublisher_h
10+
11+
#import <Foundation/Foundation.h>
12+
13+
NS_ASSUME_NONNULL_BEGIN
14+
15+
__attribute__((swift_name("Publisher")))
16+
@protocol KMPOVMPublisher
17+
- (void)send;
18+
@end
19+
20+
NS_ASSUME_NONNULL_END
21+
22+
#endif /* KMPOVMPublisher_h */
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// KMPOVMSubscriptionCount.h
3+
// KMPObservableViewModelCoreObjC
4+
//
5+
// Created by Rick Clephas on 09/06/2025.
6+
//
7+
8+
#ifndef KMPOVMSubscriptionCount_h
9+
#define KMPOVMSubscriptionCount_h
10+
11+
#import <Foundation/Foundation.h>
12+
13+
NS_ASSUME_NONNULL_BEGIN
14+
15+
__attribute__((swift_name("SubscriptionCount")))
16+
@protocol KMPOVMSubscriptionCount
17+
- (void)increase;
18+
- (void)decrease;
19+
@end
20+
21+
NS_ASSUME_NONNULL_END
22+
23+
#endif /* KMPOVMSubscriptionCount_h */

KMPObservableViewModelCoreObjC/KMPOVMViewModelScope.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@
99
#define KMPOVMViewModelScope_h
1010

1111
#import <Foundation/Foundation.h>
12+
#import "KMPOVMPublisher.h"
13+
#import "KMPOVMSubscriptionCount.h"
14+
15+
NS_ASSUME_NONNULL_BEGIN
1216

1317
__attribute__((swift_name("ViewModelScope")))
1418
@protocol KMPOVMViewModelScope
15-
- (void)increaseSubscriptionCount;
16-
- (void)decreaseSubscriptionCount;
17-
- (void)setSendObjectWillChange:(void (^ _Nonnull)(void))sendObjectWillChange;
19+
@property (readonly) id<KMPOVMSubscriptionCount> subscriptionCount;
20+
@property id<KMPOVMPublisher> _Nullable publisher;
1821
@end
1922

23+
NS_ASSUME_NONNULL_END
24+
2025
#endif /* KMPOVMViewModelScope_h */

KMPObservableViewModelCoreObjC/KMPObservableViewModelCoreObjC.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
// Created by Rick Clephas on 27/11/2022.
66
//
77

8+
#import "KMPOVMPublisher.h"
9+
#import "KMPOVMSubscriptionCount.h"
810
#import "KMPOVMViewModelScope.h"

0 commit comments

Comments
 (0)