Skip to content

Commit 8e9ffae

Browse files
committed
Merge branch 'master' into feature/kotlin-1.9.0
2 parents 89f4d92 + 6f43fa7 commit 8e9ffae

File tree

9 files changed

+275
-25
lines changed

9 files changed

+275
-25
lines changed

KMMViewModel.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
1D198B302933C01800EF778D /* KMMVMViewModelScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1D198B2E2933C01800EF778D /* KMMVMViewModelScope.h */; settings = {ATTRIBUTES = (Public, ); }; };
1818
1D198B312933C01800EF778D /* KMMViewModelCoreObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D198B2F2933C01800EF778D /* KMMViewModelCoreObjC.m */; };
1919
1D198B322933C04400EF778D /* KMMViewModelCoreObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D198B272933BFD900EF778D /* KMMViewModelCoreObjC.framework */; };
20+
1D6641DD2A5175C3000180D7 /* ChildViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6641DC2A5175C3000180D7 /* ChildViewModels.swift */; };
2021
1DDAF21E293545DD0049C114 /* KMMViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDAF21D293545DD0049C114 /* KMMViewModel.swift */; };
2122
/* End PBXBuildFile section */
2223

@@ -49,6 +50,7 @@
4950
1D198B292933BFD900EF778D /* KMMViewModelCoreObjC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMMViewModelCoreObjC.h; sourceTree = "<group>"; };
5051
1D198B2E2933C01800EF778D /* KMMVMViewModelScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMMVMViewModelScope.h; sourceTree = "<group>"; };
5152
1D198B2F2933C01800EF778D /* KMMViewModelCoreObjC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMMViewModelCoreObjC.m; sourceTree = "<group>"; };
53+
1D6641DC2A5175C3000180D7 /* ChildViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChildViewModels.swift; sourceTree = "<group>"; };
5254
1DDAF21D293545DD0049C114 /* KMMViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMMViewModel.swift; sourceTree = "<group>"; };
5355
/* End PBXFileReference section */
5456

@@ -105,6 +107,7 @@
105107
children = (
106108
1D0DA80129336AD40057DDAD /* ObservableViewModel.swift */,
107109
1DDAF21D293545DD0049C114 /* KMMViewModel.swift */,
110+
1D6641DC2A5175C3000180D7 /* ChildViewModels.swift */,
108111
);
109112
path = KMMViewModelCore;
110113
sourceTree = "<group>";
@@ -294,6 +297,7 @@
294297
buildActionMask = 2147483647;
295298
files = (
296299
1DDAF21E293545DD0049C114 /* KMMViewModel.swift in Sources */,
300+
1D6641DD2A5175C3000180D7 /* ChildViewModels.swift in Sources */,
297301
1D0DA80229336AD40057DDAD /* ObservableViewModel.swift in Sources */,
298302
);
299303
runOnlyForDeploymentPostprocessing = 0;

KMMViewModelCore.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 = 'KMMViewModelCore'
3-
s.version = '1.0.0-ALPHA-9'
3+
s.version = '1.0.0-ALPHA-10'
44
s.summary = 'Library to share Kotlin ViewModels with Swift'
55

66
s.homepage = 'https://github.com/rickclephas/KMM-ViewModel'
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//
2+
// ChildViewModels.swift
3+
// KMMViewModelCore
4+
//
5+
// Created by Rick Clephas on 02/07/2023.
6+
//
7+
8+
import KMMViewModelCoreObjC
9+
10+
public extension KMMViewModel {
11+
12+
private func setChildViewModels(_ keyPath: AnyKeyPath, _ viewModels: AnyHashable?) {
13+
if let viewModels = viewModels {
14+
observableViewModel(for: self).childViewModels[keyPath] = viewModels
15+
} else {
16+
observableViewModel(for: self).childViewModels.removeValue(forKey: keyPath)
17+
}
18+
}
19+
20+
private func setChildViewModel<ViewModel: KMMViewModel>(
21+
_ viewModel: ViewModel?,
22+
at keyPath: AnyKeyPath
23+
) {
24+
setChildViewModels(keyPath, observableViewModel(for: viewModel))
25+
}
26+
27+
/// Stores a reference to the `ObservableObject` for the specified child `KMMViewModel`.
28+
func childViewModel<ViewModel: KMMViewModel>(
29+
_ viewModel: ViewModel?,
30+
at keyPath: KeyPath<Self, ViewModel?>
31+
) -> ViewModel? {
32+
setChildViewModel(viewModel, at: keyPath)
33+
return viewModel
34+
}
35+
36+
/// Stores a reference to the `ObservableObject` for the specified child `KMMViewModel`.
37+
func childViewModel<ViewModel: KMMViewModel>(
38+
_ viewModel: ViewModel,
39+
at keyPath: KeyPath<Self, ViewModel>
40+
) -> ViewModel {
41+
setChildViewModel(viewModel, at: keyPath)
42+
return viewModel
43+
}
44+
45+
// MARK: Arrays
46+
47+
private func setChildViewModels<ViewModel: KMMViewModel>(
48+
_ viewModels: [ViewModel?]?,
49+
at keyPath: AnyKeyPath
50+
) {
51+
setChildViewModels(keyPath, viewModels?.map { viewModel in
52+
observableViewModel(for: viewModel)
53+
})
54+
}
55+
56+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
57+
func childViewModels<ViewModel: KMMViewModel>(
58+
_ viewModels: [ViewModel?]?,
59+
at keyPath: KeyPath<Self, [ViewModel?]?>
60+
) -> [ViewModel?]? {
61+
setChildViewModels(viewModels, at: keyPath)
62+
return viewModels
63+
}
64+
65+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
66+
func childViewModels<ViewModel: KMMViewModel>(
67+
_ viewModels: [ViewModel?],
68+
at keyPath: KeyPath<Self, [ViewModel?]>
69+
) -> [ViewModel?] {
70+
setChildViewModels(viewModels, at: keyPath)
71+
return viewModels
72+
}
73+
74+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
75+
func childViewModels<ViewModel: KMMViewModel>(
76+
_ viewModels: [ViewModel]?,
77+
at keyPath: KeyPath<Self, [ViewModel]?>
78+
) -> [ViewModel]? {
79+
setChildViewModels(viewModels, at: keyPath)
80+
return viewModels
81+
}
82+
83+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
84+
func childViewModels<ViewModel: KMMViewModel>(
85+
_ viewModels: [ViewModel],
86+
at keyPath: KeyPath<Self, [ViewModel]>
87+
) -> [ViewModel] {
88+
setChildViewModels(viewModels, at: keyPath)
89+
return viewModels
90+
}
91+
92+
// MARK: Sets
93+
94+
private func setChildViewModels<ViewModel: KMMViewModel>(
95+
_ viewModels: Set<ViewModel?>?,
96+
at keyPath: AnyKeyPath
97+
) {
98+
setChildViewModels(keyPath, viewModels?.map { viewModel in
99+
observableViewModel(for: viewModel)
100+
})
101+
}
102+
103+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
104+
func childViewModels<ViewModel: KMMViewModel>(
105+
_ viewModels: Set<ViewModel?>?,
106+
at keyPath: KeyPath<Self, Set<ViewModel?>?>
107+
) -> Set<ViewModel?>? {
108+
setChildViewModels(viewModels, at: keyPath)
109+
return viewModels
110+
}
111+
112+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
113+
func childViewModels<ViewModel: KMMViewModel>(
114+
_ viewModels: Set<ViewModel?>,
115+
at keyPath: KeyPath<Self, Set<ViewModel?>>
116+
) -> Set<ViewModel?> {
117+
setChildViewModels(viewModels, at: keyPath)
118+
return viewModels
119+
}
120+
121+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
122+
func childViewModels<ViewModel: KMMViewModel>(
123+
_ viewModels: Set<ViewModel>?,
124+
at keyPath: KeyPath<Self, Set<ViewModel>?>
125+
) -> Set<ViewModel>? {
126+
setChildViewModels(viewModels, at: keyPath)
127+
return viewModels
128+
}
129+
130+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
131+
func childViewModels<ViewModel: KMMViewModel>(
132+
_ viewModels: Set<ViewModel>,
133+
at keyPath: KeyPath<Self, Set<ViewModel>>
134+
) -> Set<ViewModel> {
135+
setChildViewModels(viewModels, at: keyPath)
136+
return viewModels
137+
}
138+
139+
// MARK: Dictionaries
140+
141+
private func setChildViewModels<Key, ViewModel: KMMViewModel>(
142+
_ viewModels: [Key : ViewModel?]?,
143+
at keyPath: AnyKeyPath
144+
) {
145+
setChildViewModels(keyPath, viewModels?.mapValues { viewModel in
146+
observableViewModel(for: viewModel)
147+
})
148+
}
149+
150+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
151+
func childViewModels<Key, ViewModel: KMMViewModel>(
152+
_ viewModels: [Key : ViewModel?]?,
153+
at keyPath: KeyPath<Self, [Key : ViewModel?]?>
154+
) -> [Key : ViewModel?]? {
155+
setChildViewModels(viewModels, at: keyPath)
156+
return viewModels
157+
}
158+
159+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
160+
func childViewModels<Key, ViewModel: KMMViewModel>(
161+
_ viewModels: [Key : ViewModel?],
162+
at keyPath: KeyPath<Self, [Key : ViewModel?]>
163+
) -> [Key : ViewModel?] {
164+
setChildViewModels(viewModels, at: keyPath)
165+
return viewModels
166+
}
167+
168+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
169+
func childViewModels<Key, ViewModel: KMMViewModel>(
170+
_ viewModels: [Key : ViewModel]?,
171+
at keyPath: KeyPath<Self, [Key : ViewModel]?>
172+
) -> [Key : ViewModel]? {
173+
setChildViewModels(viewModels, at: keyPath)
174+
return viewModels
175+
}
176+
177+
/// Stores references to the `ObservableObject`s of the specified child `KMMViewModel`s.
178+
func childViewModels<Key, ViewModel: KMMViewModel>(
179+
_ viewModels: [Key : ViewModel],
180+
at keyPath: KeyPath<Self, [Key : ViewModel]>
181+
) -> [Key : ViewModel] {
182+
setChildViewModels(viewModels, at: keyPath)
183+
return viewModels
184+
}
185+
}

KMMViewModelCore/ObservableViewModel.swift

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,63 @@ public func createObservableViewModel<ViewModel: KMMViewModel>(
1717

1818
private var observableViewModelKey = "observableViewModel"
1919

20-
/// Gets an `ObservableObject` for the specified `KMMViewModel`.
20+
private class WeakObservableViewModel<ViewModel: KMMViewModel> {
21+
weak var observableViewModel: ObservableViewModel<ViewModel>?
22+
init(_ observableViewModel: ObservableViewModel<ViewModel>) {
23+
self.observableViewModel = observableViewModel
24+
}
25+
}
26+
27+
/// Gets the `ObservableObject` for the specified `KMMViewModel`.
2128
/// - Parameter viewModel: The `KMMViewModel` to wrap in an `ObservableObject`.
2229
public func observableViewModel<ViewModel: KMMViewModel>(
2330
for viewModel: ViewModel
2431
) -> ObservableViewModel<ViewModel> {
25-
if let observableViewModel = objc_getAssociatedObject(viewModel, &observableViewModelKey) {
26-
return observableViewModel as! ObservableViewModel<ViewModel>
32+
if let object = objc_getAssociatedObject(viewModel, &observableViewModelKey) {
33+
guard let observableViewModel = (object as! WeakObservableViewModel<ViewModel>).observableViewModel else {
34+
fatalError("ObservableViewModel has been deallocated")
35+
}
36+
return observableViewModel
37+
} else {
38+
let observableViewModel = ObservableViewModel(viewModel)
39+
let object = WeakObservableViewModel<ViewModel>(observableViewModel)
40+
objc_setAssociatedObject(viewModel, &observableViewModelKey, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
41+
return observableViewModel
2742
}
28-
let observableViewModel = ObservableViewModel(viewModel)
29-
objc_setAssociatedObject(viewModel, &observableViewModelKey, observableViewModel, .OBJC_ASSOCIATION_ASSIGN)
43+
}
44+
45+
/// Gets the `ObservableObject` for the specified `KMMViewModel`.
46+
/// - Parameter viewModel: The `KMMViewModel` to wrap in an `ObservableObject`.
47+
public func observableViewModel<ViewModel: KMMViewModel>(
48+
for viewModel: ViewModel?
49+
) -> ObservableViewModel<ViewModel>? {
50+
guard let viewModel = viewModel else { return nil }
51+
let observableViewModel = observableViewModel(for: viewModel)
3052
return observableViewModel
3153
}
3254

3355
/// An `ObservableObject` for a `KMMViewModel`.
34-
public final class ObservableViewModel<ViewModel: KMMViewModel>: ObservableObject {
56+
public final class ObservableViewModel<ViewModel: KMMViewModel>: ObservableObject, Hashable {
3557

3658
public let objectWillChange: ObservableViewModelPublisher
3759

60+
/// The observed `KMMViewModel`.
3861
public let viewModel: ViewModel
3962

63+
internal var childViewModels: Dictionary<AnyKeyPath, AnyHashable> = [:]
64+
4065
internal init(_ viewModel: ViewModel) {
4166
objectWillChange = ObservableViewModelPublisher(viewModel.viewModelScope, viewModel.objectWillChange)
4267
self.viewModel = viewModel
4368
}
69+
70+
public static func == (lhs: ObservableViewModel<ViewModel>, rhs: ObservableViewModel<ViewModel>) -> Bool {
71+
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
72+
}
73+
74+
public func hash(into hasher: inout Hasher) {
75+
hasher.combine(ObjectIdentifier(self))
76+
}
4477
}
4578

4679
/// Publisher for `ObservableViewModel` that connects to the `ViewModelScope`.
@@ -53,7 +86,7 @@ public final class ObservableViewModelPublisher: Publisher {
5386
private let publisher = ObservableObjectPublisher()
5487
private var objectWillChangeCancellable: AnyCancellable? = nil
5588

56-
init(_ viewModelScope: ViewModelScope, _ objectWillChange: ObservableObjectPublisher) {
89+
internal init(_ viewModelScope: ViewModelScope, _ objectWillChange: ObservableObjectPublisher) {
5790
self.viewModelScope = viewModelScope
5891
viewModelScope.setSendObjectWillChange { [weak self] in
5992
self?.publisher.send()

KMMViewModelCoreObjC.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 = 'KMMViewModelCoreObjC'
3-
s.version = '1.0.0-ALPHA-9'
3+
s.version = '1.0.0-ALPHA-10'
44
s.summary = 'Library to share Kotlin ViewModels with Swift'
55

66
s.homepage = 'https://github.com/rickclephas/KMM-ViewModel'

KMMViewModelSwiftUI.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 = 'KMMViewModelSwiftUI'
3-
s.version = '1.0.0-ALPHA-9'
3+
s.version = '1.0.0-ALPHA-10'
44
s.summary = 'Library to share Kotlin ViewModels with SwiftUI'
55

66
s.homepage = 'https://github.com/rickclephas/KMM-ViewModel'

README.md

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ A library that allows you to share ViewModels between Android and iOS.
44

55
## Compatibility
66

7-
The latest version of the library uses Kotlin version `1.8.21`.
8-
Compatibility versions for newer Kotlin versions are also available:
9-
10-
| Version | Version suffix | Kotlin | Coroutines | AndroidX Lifecycle |
11-
|---------------|-----------------|:----------:|:----------:|:------------------:|
12-
| **_latest_** | **_no suffix_** | **1.8.21** | **1.7.1** | **2.5.1** |
13-
| 1.0.0-ALPHA-8 | _no suffix_ | 1.8.21 | 1.7.0 | 2.5.1 |
14-
| 1.0.0-ALPHA-7 | _no suffix_ | 1.8.21 | 1.6.4 | 2.5.1 |
15-
| 1.0.0-ALPHA-6 | _no suffix_ | 1.8.20 | 1.6.4 | 2.5.1 |
16-
| 1.0.0-ALPHA-4 | _no suffix_ | 1.8.10 | 1.6.4 | 2.5.1 |
17-
| 1.0.0-ALPHA-3 | _no suffix_ | 1.8.0 | 1.6.4 | 2.5.1 |
7+
The latest version of the library uses Kotlin version `1.8.22`.
8+
Compatibility versions for older and/or preview Kotlin versions are also available:
9+
10+
| Version | Version suffix | Kotlin | Coroutines | AndroidX Lifecycle |
11+
|---------------|--------------------|:----------:|:----------:|:------------------:|
12+
| _latest_ | -kotlin-1.9.0-RC | 1.9.0-RC | 1.7.2 | 2.6.1 |
13+
| **_latest_** | **_no suffix_** | **1.8.22** | **1.7.2** | **2.6.1** |
14+
| 1.0.0-ALPHA-9 | -kotlin-1.9.0-Beta | 1.9.0-Beta | 1.7.1 | 2.6.1 |
15+
| 1.0.0-ALPHA-9 | _no suffix_ | 1.8.21 | 1.7.1 | 2.5.1 |
16+
| 1.0.0-ALPHA-8 | _no suffix_ | 1.8.21 | 1.7.0 | 2.5.1 |
17+
| 1.0.0-ALPHA-7 | _no suffix_ | 1.8.21 | 1.6.4 | 2.5.1 |
18+
| 1.0.0-ALPHA-6 | _no suffix_ | 1.8.20 | 1.6.4 | 2.5.1 |
19+
| 1.0.0-ALPHA-4 | _no suffix_ | 1.8.10 | 1.6.4 | 2.5.1 |
20+
| 1.0.0-ALPHA-3 | _no suffix_ | 1.8.0 | 1.6.4 | 2.5.1 |
1821

1922
## Kotlin
2023

@@ -80,7 +83,7 @@ And the second being a different `MutableStateFlow` constructor:
8083
+ private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)
8184
```
8285

83-
These minor differences will make sure that any state changes are propagate to iOS.
86+
These minor differences will make sure that any state changes are propagated to iOS.
8487

8588
> **Note**: `viewModelScope` is a wrapper around the actual `CoroutineScope` which can be accessed
8689
> via the `ViewModelScope.coroutineScope` property.
@@ -181,3 +184,28 @@ class TimeTravelViewModel: shared.TimeTravelViewModel {
181184
@Published var isResetDisabled: Bool = false
182185
}
183186
```
187+
188+
### Child view models
189+
190+
You'll need some additional logic if your `KMMViewModel`s expose child view models.
191+
192+
First make sure to use the `NativeCoroutinesRefinedState` annotation instead of the `NativeCoroutinesState` annotation:
193+
```kotlin
194+
class MyParentViewModel: KMMViewModel() {
195+
@NativeCoroutinesRefinedState
196+
val myChildViewModel: StateFlow<MyChildViewModel?> = MutableStateFlow(null)
197+
}
198+
```
199+
200+
After that you should create a Swift extension property using the `childViewModel(_, at:)` function:
201+
```swift
202+
extension MyParentViewModel {
203+
var myChildViewModel: MyChildViewModel? {
204+
childViewModel(__myChildViewModel, at: \.__myChildViewModel)
205+
}
206+
}
207+
```
208+
209+
This will prevent your Swift view models from being deallocated too soon.
210+
211+
> **Note**: for lists, sets and dictionaries containing view models there is `childViewModels(_, at:)`.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ buildscript {
1515

1616
allprojects {
1717
group = "com.rickclephas.kmm"
18-
version = "1.0.0-ALPHA-9-kotlin-1.9.0-RC"
18+
version = "1.0.0-ALPHA-10"
1919

2020
repositories {
2121
mavenCentral()

0 commit comments

Comments
 (0)