Skip to content

V1 API #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
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
22 changes: 7 additions & 15 deletions KMPObservableViewModelCore/ChildViewModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@ import KMPObservableViewModelCoreObjC

public extension ViewModel {

private func setChildViewModelPublishers(_ keyPath: AnyKeyPath, _ publishers: AnyHashable?) {
if let publishers = publishers {
observableViewModelPublishers(for: self).childPublishers[keyPath] = publishers
} else {
observableViewModelPublishers(for: self).childPublishers.removeValue(forKey: keyPath)
}
}

private func setChildViewModel<VM: ViewModel>(
_ viewModel: VM?,
at keyPath: AnyKeyPath
) {
setChildViewModelPublishers(keyPath, observableViewModelPublishers(for: viewModel))
viewModelWillChange.cancellable.setChildCancellables(keyPath, ViewModelCancellable.get(for: viewModel))
}

/// Stores a reference to the `ObservableObject` for the specified child `ViewModel`.
Expand All @@ -48,8 +40,8 @@ public extension ViewModel {
_ viewModels: [VM?]?,
at keyPath: AnyKeyPath
) {
setChildViewModelPublishers(keyPath, viewModels?.map { viewModel in
observableViewModelPublishers(for: viewModel)
viewModelWillChange.cancellable.setChildCancellables(keyPath, viewModels?.map { viewModel in
ViewModelCancellable.get(for: viewModel)
})
}

Expand Down Expand Up @@ -95,8 +87,8 @@ public extension ViewModel {
_ viewModels: Set<VM?>?,
at keyPath: AnyKeyPath
) {
setChildViewModelPublishers(keyPath, viewModels?.map { viewModel in
observableViewModelPublishers(for: viewModel)
viewModelWillChange.cancellable.setChildCancellables(keyPath, viewModels?.map { viewModel in
ViewModelCancellable.get(for: viewModel)
})
}

Expand Down Expand Up @@ -142,8 +134,8 @@ public extension ViewModel {
_ viewModels: [Key : VM?]?,
at keyPath: AnyKeyPath
) {
setChildViewModelPublishers(keyPath, viewModels?.mapValues { viewModel in
observableViewModelPublishers(for: viewModel)
viewModelWillChange.cancellable.setChildCancellables(keyPath, viewModels?.mapValues { viewModel in
ViewModelCancellable.get(for: viewModel)
})
}

Expand Down
13 changes: 6 additions & 7 deletions KMPObservableViewModelCore/ObservableViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import KMPObservableViewModelCoreObjC
public func observableViewModel<VM: ViewModel>(
for viewModel: VM
) -> ObservableViewModel<VM> {
let publishers = observableViewModelPublishers(for: viewModel)
return ObservableViewModel(publishers, viewModel)
return ObservableViewModel(viewModel)
}

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

/// Holds a strong reference to the publishers
private let publishers: ObservableViewModelPublishers
/// Holds a strong reference to the cancellable
private let cancellable: AnyCancellable

internal init(_ publishers: ObservableViewModelPublishers, _ viewModel: VM) {
objectWillChange = publishers.publisher
internal init(_ viewModel: VM) {
objectWillChange = viewModel.viewModelWillChange
self.viewModel = viewModel
self.publishers = publishers
cancellable = ViewModelCancellable.get(for: viewModel)
}

public static func == (lhs: ObservableViewModel<VM>, rhs: ObservableViewModel<VM>) -> Bool {
Expand Down
52 changes: 20 additions & 32 deletions KMPObservableViewModelCore/ObservableViewModelPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,27 @@ import Combine
import KMPObservableViewModelCoreObjC

/// Publisher for `ObservableViewModel` that connects to the `ViewModelScope`.
public final class ObservableViewModelPublisher: Publisher {
public final class ObservableViewModelPublisher: Combine.Publisher, KMPObservableViewModelCoreObjC.Publisher {
public typealias Output = Void
public typealias Failure = Never

internal weak var viewModel: (any ViewModel)?
internal let cancellable = ViewModelCancellable()

private let publisher = ObservableObjectPublisher()
private var objectWillChangeCancellable: AnyCancellable? = nil
private let publisher: ObservableObjectPublisher
private let subscriptionCount: any SubscriptionCount

internal init(_ viewModel: any ViewModel, _ objectWillChange: ObservableObjectPublisher) {
self.viewModel = viewModel
viewModel.viewModelScope.setSendObjectWillChange { [weak self] in
self?.publisher.send()
}
objectWillChangeCancellable = objectWillChange.sink { [weak self] _ in
self?.publisher.send()
}
internal init(_ viewModel: any ViewModel) {
self.publisher = viewModel.objectWillChange
self.subscriptionCount = viewModel.viewModelScope.subscriptionCount
}

public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Void == S.Input {
viewModel?.viewModelScope.increaseSubscriptionCount()
publisher.receive(subscriber: ObservableViewModelSubscriber(self, subscriber))
subscriptionCount.increase()
publisher.receive(subscriber: ObservableViewModelSubscriber(subscriptionCount, subscriber))
}

deinit {
guard let viewModel else { return }
if let cancellable = viewModel as? Cancellable {
cancellable.cancel()
}
viewModel.clear()
public func send() {
publisher.send()
}
}

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

private let publisher: ObservableViewModelPublisher
private let subscriptionCount: any SubscriptionCount
private let subscriber: S

init(_ publisher: ObservableViewModelPublisher, _ subscriber: S) {
self.publisher = publisher
init(_ subscriptionCount: any SubscriptionCount, _ subscriber: S) {
self.subscriptionCount = subscriptionCount
self.subscriber = subscriber
}

func receive(subscription: Subscription) {
subscriber.receive(subscription: ObservableViewModelSubscription(publisher, subscription))
subscriber.receive(subscription: ObservableViewModelSubscription(subscriptionCount, subscription))
}

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

private let publisher: ObservableViewModelPublisher
private var subscriptionCount: (any SubscriptionCount)?
private let subscription: Subscription

init(_ publisher: ObservableViewModelPublisher, _ subscription: Subscription) {
self.publisher = publisher
init(_ subscriptionCount: any SubscriptionCount, _ subscription: Subscription) {
self.subscriptionCount = subscriptionCount
self.subscription = subscription
}

func request(_ demand: Subscribers.Demand) {
subscription.request(demand)
}

private var cancelled = false

func cancel() {
subscription.cancel()
guard !cancelled else { return }
cancelled = true
publisher.viewModel?.viewModelScope.decreaseSubscriptionCount()
subscriptionCount?.decrease()
subscriptionCount = nil
}
}
62 changes: 0 additions & 62 deletions KMPObservableViewModelCore/ObservableViewModelPublishers.swift

This file was deleted.

15 changes: 14 additions & 1 deletion KMPObservableViewModelCore/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@
import Combine
import KMPObservableViewModelCoreObjC

/// A Kotlin Multiplatform Mobile ViewModel.
/// A Kotlin Multiplatform ViewModel.
public protocol ViewModel: ObservableObject where ObjectWillChangePublisher == ObservableObjectPublisher {
/// The `ViewModelScope` of this `ViewModel`.
var viewModelScope: ViewModelScope { get }
/// An `ObservableViewModelPublisher` that emits before this `ViewModel` has changed.
var viewModelWillChange: ObservableViewModelPublisher { get }
/// Internal KMP-ObservableViewModel function used to clear the ViewModel.
/// - Warning: You should NOT call this yourself!
func clear()
}

public extension ViewModel {
var viewModelWillChange: ObservableViewModelPublisher {
if let publisher = viewModelScope.publisher {
return publisher as! ObservableViewModelPublisher
}
let publisher = ObservableViewModelPublisher(self)
viewModelScope.publisher = publisher
return publisher
}
}
53 changes: 53 additions & 0 deletions KMPObservableViewModelCore/ViewModelCancellable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// ViewModelCancellable.swift
// KMPObservableViewModelCore
//
// Created by Rick Clephas on 09/06/2025.
//

import Combine

/// Helper object that provides a weakly cached `Cancellable` for a `ViewModel`.
internal class ViewModelCancellable {

private var didInit = false
private weak var cancellable: AnyCancellable?
private var childCancellables: Dictionary<AnyKeyPath, AnyHashable>? = [:]

private func get(_ viewModel: any ViewModel) -> AnyCancellable {
guard didInit else {
let cancellable = AnyCancellable {
if let cancellable = viewModel as? Cancellable {
cancellable.cancel()
}
self.childCancellables = nil
viewModel.clear()
}
self.cancellable = cancellable
didInit = true
return cancellable
}
guard let cancellable = self.cancellable else {
fatalError("ObservableViewModel for \(viewModel) has been deallocated")
}
return cancellable
}

func setChildCancellables(_ keyPath: AnyKeyPath, _ cancellables: AnyHashable?) {
if let cancellables = cancellables {
childCancellables?[keyPath] = cancellables
} else {
childCancellables?.removeValue(forKey: keyPath)
}
}

static func get(for viewModel: any ViewModel) -> AnyCancellable {
viewModel.viewModelWillChange.cancellable.get(viewModel)
}

static func get(for viewModel: (any ViewModel)?) -> AnyCancellable? {
guard let viewModel else { return nil }
let cancellable = get(for: viewModel)
return cancellable
}
}
22 changes: 22 additions & 0 deletions KMPObservableViewModelCoreObjC/KMPOVMPublisher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// KMPOVMPublisher.h
// KMPObservableViewModelCoreObjC
//
// Created by Rick Clephas on 09/06/2025.
//

#ifndef KMPOVMPublisher_h
#define KMPOVMPublisher_h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

__attribute__((swift_name("Publisher")))
@protocol KMPOVMPublisher
- (void)send;
@end

NS_ASSUME_NONNULL_END

#endif /* KMPOVMPublisher_h */
23 changes: 23 additions & 0 deletions KMPObservableViewModelCoreObjC/KMPOVMSubscriptionCount.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// KMPOVMSubscriptionCount.h
// KMPObservableViewModelCoreObjC
//
// Created by Rick Clephas on 09/06/2025.
//

#ifndef KMPOVMSubscriptionCount_h
#define KMPOVMSubscriptionCount_h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

__attribute__((swift_name("SubscriptionCount")))
@protocol KMPOVMSubscriptionCount
- (void)increase;
- (void)decrease;
@end

NS_ASSUME_NONNULL_END

#endif /* KMPOVMSubscriptionCount_h */
11 changes: 8 additions & 3 deletions KMPObservableViewModelCoreObjC/KMPOVMViewModelScope.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
#define KMPOVMViewModelScope_h

#import <Foundation/Foundation.h>
#import "KMPOVMPublisher.h"
#import "KMPOVMSubscriptionCount.h"

NS_ASSUME_NONNULL_BEGIN

__attribute__((swift_name("ViewModelScope")))
@protocol KMPOVMViewModelScope
- (void)increaseSubscriptionCount;
- (void)decreaseSubscriptionCount;
- (void)setSendObjectWillChange:(void (^ _Nonnull)(void))sendObjectWillChange;
@property (readonly) id<KMPOVMSubscriptionCount> subscriptionCount;
@property id<KMPOVMPublisher> _Nullable publisher;
@end

NS_ASSUME_NONNULL_END

#endif /* KMPOVMViewModelScope_h */
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
// Created by Rick Clephas on 27/11/2022.
//

#import "KMPOVMPublisher.h"
#import "KMPOVMSubscriptionCount.h"
#import "KMPOVMViewModelScope.h"
Loading
Loading