Skip to content

Commit d5fcee4

Browse files
committed
Generic viewmodel function
Move IOSViewModelStoreOwner to Swift # Conflicts: # Fruitties/iosApp/iosApp/CartView.swift # Fruitties/iosApp/iosApp/ContentView.swift # Fruitties/iosApp/iosApp/ViewModelStoreOwnerProvider.swift # Fruitties/iosApp/iosApp/iOSApp.swift # Fruitties/shared/src/iosMain/kotlin/com/example/fruitties/di/viewmodel/IOSViewModelStoreOwner.kt
1 parent 54efca6 commit d5fcee4

File tree

13 files changed

+258
-107
lines changed

13 files changed

+258
-107
lines changed

Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/CartScreen.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import androidx.compose.material3.TopAppBarDefaults
4141
import androidx.compose.runtime.Composable
4242
import androidx.compose.runtime.collectAsState
4343
import androidx.compose.runtime.getValue
44-
import androidx.compose.runtime.remember
4544
import androidx.compose.ui.Alignment
4645
import androidx.compose.ui.Modifier
4746
import androidx.compose.ui.platform.LocalContext
@@ -51,6 +50,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
5150
import com.example.fruitties.android.R
5251
import com.example.fruitties.android.di.App
5352
import com.example.fruitties.viewmodel.CartViewModel
53+
import com.example.fruitties.viewmodel.creationExtras
5454

5555
@OptIn(ExperimentalMaterial3Api::class)
5656
@Composable
@@ -61,13 +61,10 @@ fun CartScreen(onNavBarBack: () -> Unit) {
6161
// Here we put the KMP-compatible AppContainer into the extras
6262
// so it can be passed to the ViewModel factory.
6363
val app = LocalContext.current.applicationContext as App
64-
val extras = remember(app) {
65-
val container = app.container
66-
CartViewModel.creationExtras(container)
67-
}
64+
6865
val viewModel: CartViewModel = viewModel(
6966
factory = CartViewModel.Factory,
70-
extras = extras,
67+
extras = creationExtras(app.container),
7168
)
7269

7370
val cartState by viewModel.cartUiState.collectAsState()

Fruitties/androidApp/src/main/java/com/example/fruitties/android/ui/ListScreen.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import com.example.fruitties.android.R
5353
import com.example.fruitties.android.di.App
5454
import com.example.fruitties.model.Fruittie
5555
import com.example.fruitties.viewmodel.MainViewModel
56+
import com.example.fruitties.viewmodel.creationExtras
5657

5758
@OptIn(ExperimentalMaterial3Api::class)
5859
@Composable
@@ -63,13 +64,9 @@ fun ListScreen(onClickViewCart: () -> Unit = {}) {
6364
// Here we put the KMP-compatible AppContainer into the extras
6465
// so it can be passed to the ViewModel factory.
6566
val app = LocalContext.current.applicationContext as App
66-
val extras = remember(app) {
67-
val container = app.container
68-
MainViewModel.creationExtras(container)
69-
}
7067
val viewModel: MainViewModel = viewModel(
7168
factory = MainViewModel.Factory,
72-
extras = extras,
69+
extras = creationExtras(app.container),
7370
)
7471

7572
val uiState by viewModel.homeUiState.collectAsState()

Fruitties/iosApp/iosApp.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
1111
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
1212
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
13+
2DBC08C52E0C3291000309C8 /* ViewModelStoreProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBC08C42E0C3291000309C8 /* ViewModelStoreProvider.swift */; };
14+
2DBC08C72E0C32CE000309C8 /* ObservableValueWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBC08C62E0C32CE000309C8 /* ObservableValueWrapper.swift */; };
1315
2E8773602BC85C2400BF7C40 /* CartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E87735F2BC85C2400BF7C40 /* CartView.swift */; };
1416
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
1517
/* End PBXBuildFile section */
@@ -31,6 +33,8 @@
3133
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3234
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
3335
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
36+
2DBC08C42E0C3291000309C8 /* ViewModelStoreProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelStoreProvider.swift; sourceTree = "<group>"; };
37+
2DBC08C62E0C32CE000309C8 /* ObservableValueWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValueWrapper.swift; sourceTree = "<group>"; };
3438
2E87735F2BC85C2400BF7C40 /* CartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartView.swift; sourceTree = "<group>"; };
3539
7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
3640
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -82,6 +86,8 @@
8286
2152FB032600AC8F00CF470E /* iOSApp.swift */,
8387
058557D7273AAEEB004C7B11 /* Preview Content */,
8488
2E87735F2BC85C2400BF7C40 /* CartView.swift */,
89+
2DBC08C42E0C3291000309C8 /* ViewModelStoreProvider.swift */,
90+
2DBC08C62E0C32CE000309C8 /* ObservableValueWrapper.swift */,
8591
);
8692
path = iosApp;
8793
sourceTree = "<group>";
@@ -190,6 +196,8 @@
190196
2E8773602BC85C2400BF7C40 /* CartView.swift in Sources */,
191197
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
192198
7555FF83242A565900829871 /* ContentView.swift in Sources */,
199+
2DBC08C72E0C32CE000309C8 /* ObservableValueWrapper.swift in Sources */,
200+
2DBC08C52E0C3291000309C8 /* ViewModelStoreProvider.swift in Sources */,
193201
);
194202
runOnlyForDeploymentPostprocessing = 0;
195203
};

Fruitties/iosApp/iosApp/CartView.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,28 @@ import SwiftUI
1919
import shared
2020

2121
struct CartView : View {
22-
let cartViewModel: CartViewModel
22+
/// Injects the `IOSViewModelStoreOwner` from the environment, which manages the lifecycle of `ViewModel` instances.
23+
@EnvironmentObject var viewModelStoreOwner: IOSViewModelStoreOwner
2324

24-
// The ViewModel exposes a StateFlow that we access in SwiftUI with SKIE Observing.
25-
// https://skie.touchlab.co/features/flows-in-swiftui
25+
/// Injects the `AppContainer` from the environment, providing access to application-wide dependencies.
26+
@EnvironmentObject var appContainer: ObservableValueWrapper<AppContainer>
2627

2728
var body: some View {
28-
// https://skie.touchlab.co/features/flows-in-swiftui
29+
/// Retrieves the `CartViewModel` instance using the `viewModelStoreOwner`.
30+
/// The `CartViewModel.Factory` and `creationExtras` are provided to enable dependency injection
31+
/// and proper initialization of the ViewModel with its required `AppContainer`.
32+
let cartViewModel: CartViewModel = viewModelStoreOwner.viewModel(
33+
factory: CartViewModel.companion.Factory,
34+
extras: creationExtras(appContainer: appContainer.value)
35+
)
36+
37+
/// Observes the `cartUiState` `StateFlow` from the `CartViewModel` using SKIE's `Observing` utility.
38+
/// This allows SwiftUI to react to changes in the cart's UI state.
39+
/// For more details, refer to: https://skie.touchlab.co/features/flows-in-swiftui
2940
Observing(cartViewModel.cartUiState) { cartUIState in
3041
VStack {
3142
HStack {
32-
let total = cartUIState.totalItemCount
43+
let total = cartUIState.cartDetails.reduce(0) { $0 + $1.count }
3344
Text("Cart has \(total) items").padding()
3445
Spacer()
3546
}
@@ -44,7 +55,9 @@ struct CartDetailsView: View {
4455
let cartViewModel: CartViewModel
4556

4657
var body: some View {
47-
// https://skie.touchlab.co/features/flows-in-swiftui
58+
/// Observes the `cartUiState` `StateFlow` from the `CartViewModel` using SKIE's `Observing` utility.
59+
/// This allows SwiftUI to react to changes in the cart's UI state.
60+
/// For more details, refer to: https://skie.touchlab.co/features/flows-in-swiftui
4861
Observing(self.cartViewModel.cartUiState) { cartUIState in
4962
ScrollView {
5063
LazyVStack {

Fruitties/iosApp/iosApp/ContentView.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,27 @@ import shared
1919
import Foundation
2020

2121
struct ContentView: View {
22-
var mainViewModel: MainViewModel
23-
var cartViewModel: CartViewModel
22+
/// Injects the `IOSViewModelStoreOwner` from the environment, which manages the lifecycle of `ViewModel` instances.
23+
@EnvironmentObject var viewModelStoreOwner: IOSViewModelStoreOwner
24+
25+
/// Injects the `AppContainer` from the environment, providing access to application-wide dependencies.
26+
@EnvironmentObject var appContainer: ObservableValueWrapper<AppContainer>
2427

2528
var body: some View {
29+
/// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`.
30+
/// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection
31+
/// and proper initialization of the ViewModel with its required `AppContainer`.
32+
let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel(
33+
factory: MainViewModel.companion.Factory,
34+
extras: creationExtras(appContainer: appContainer.value)
35+
)
2636
NavigationStack {
2737
VStack {
2838
Text("Fruitties").font(.largeTitle).fontWeight(.bold)
2939
NavigationLink {
30-
CartView(cartViewModel: cartViewModel)
40+
ViewModelStoreOwnerProvider{
41+
CartView()
42+
}
3143
} label: {
3244
Observing(mainViewModel.homeUiState) { homeUIState in
3345
let total = homeUIState.cartItemCount
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Combine
2+
import SwiftUI
3+
import shared
4+
5+
/// A generic wrapper that makes any `Value` type observable by SwiftUI.
6+
///
7+
/// Use this to wrap non-`ObservableObject` types when their changes need to update SwiftUI views.
8+
class ObservableValueWrapper<Value>: ObservableObject {
9+
10+
/// The wrapped value. Changes trigger SwiftUI view updates.
11+
@Published var value: Value
12+
13+
/// Initializes the wrapper with an initial value.
14+
init(value: Value) {
15+
self.value = value
16+
}
17+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import SwiftUI
2+
import shared
3+
4+
/// A SwiftUI `View` that provides a `ViewModelStoreOwner` to its content.
5+
///
6+
/// Manages the lifecycle of `ViewModel` instances, scoping them to this view hierarchy.
7+
/// Clears the associated `ViewModelStore` when the provider disappears.
8+
struct ViewModelStoreOwnerProvider<Content: View>: View {
9+
@StateObject private var viewModelStoreOwner: IOSViewModelStoreOwner =
10+
IOSViewModelStoreOwner()
11+
12+
private let content: Content
13+
14+
/// Initializes the provider with its content, creating a new `IOSViewModelStoreOwner`.
15+
init(@ViewBuilder content: () -> Content) {
16+
17+
self.content = content()
18+
}
19+
20+
var body: some View {
21+
content
22+
.environmentObject(viewModelStoreOwner)
23+
.onDisappear {
24+
viewModelStoreOwner.clear()
25+
}
26+
}
27+
}
28+
29+
/// A ViewModelStoreOwner specifically for iOS.
30+
/// This is used with from iOS with Kotlin Multiplatform (KMP).
31+
class IOSViewModelStoreOwner: ObservableObject {
32+
33+
var viewModelStore: Lifecycle_viewmodelViewModelStore =
34+
Lifecycle_viewmodelViewModelStore()
35+
36+
func viewModel<T: AnyObject>(
37+
factory: Lifecycle_viewmodelViewModelProviderFactory,
38+
extras: Lifecycle_viewmodelCreationExtras,
39+
) -> T {
40+
let vm =
41+
viewModelStore.getViewModel(
42+
modelClass: T.self,
43+
factory: factory,
44+
extras: extras
45+
) as! T
46+
47+
return vm
48+
}
49+
50+
func clear() {
51+
viewModelStore.clear()
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import SwiftUI
2+
import shared
3+
4+
/// A SwiftUI `View` that provides a `ViewModelStoreOwner` to its content.
5+
///
6+
/// Manages the lifecycle of `ViewModel` instances, scoping them to this view hierarchy.
7+
/// Clears the associated `ViewModelStore` when the provider disappears.
8+
struct ViewModelStoreOwnerProvider<Content: View>: View {
9+
@StateObject private var viewModelStoreOwner: IOSViewModelStoreOwner =
10+
IOSViewModelStoreOwner()
11+
12+
private let content: Content
13+
14+
/// Initializes the provider with its content, creating a new `IOSViewModelStoreOwner`.
15+
init(@ViewBuilder content: () -> Content) {
16+
17+
self.content = content()
18+
}
19+
20+
var body: some View {
21+
content
22+
.environmentObject(viewModelStoreOwner)
23+
.onDisappear {
24+
viewModelStoreOwner.clear()
25+
}
26+
}
27+
}
28+
29+
/// A ViewModelStoreOwner specifically for iOS.
30+
/// This is used with from iOS with Kotlin Multiplatform (KMP).
31+
class IOSViewModelStoreOwner: ObservableObject {
32+
33+
var viewModelStore: Lifecycle_viewmodelViewModelStore =
34+
Lifecycle_viewmodelViewModelStore()
35+
36+
func viewModel<T: AnyObject>(
37+
factory: Lifecycle_viewmodelViewModelProviderFactory,
38+
extras: Lifecycle_viewmodelCreationExtras,
39+
) -> T {
40+
let vm =
41+
viewModelStore.getViewModel(
42+
modelClass: T.self,
43+
factory: factory,
44+
extras: extras
45+
) as! T
46+
47+
return vm
48+
}
49+
50+
func clear() {
51+
viewModelStore.clear()
52+
}
53+
}

Fruitties/iosApp/iosApp/iOSApp.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,24 @@ import shared
1919

2020
@main
2121
struct iOSApp: App {
22-
let appContainer = AppContainer(factory: Factory())
22+
/// The application's dependency container, wrapped for SwiftUI observation.
23+
let appContainer: ObservableValueWrapper<AppContainer>
24+
25+
init() {
26+
let appContainer = AppContainer(factory: Factory())
27+
self.appContainer = ObservableValueWrapper<AppContainer>(
28+
value: appContainer
29+
)
30+
}
31+
2332
var body: some Scene {
2433
WindowGroup {
25-
let iosViewModelOwner = IOSViewModelStoreOwner(appContainer: appContainer)
26-
ContentView(mainViewModel: iosViewModelOwner.getMainViewModel(),
27-
cartViewModel: iosViewModelOwner.getCartViewModel())
34+
/// Provides the root `ViewModelStoreOwner` to the environment, making it accessible to all child views.
35+
/// Nested `ViewModelStoreOwnerProvider` instances can create additional, scoped ViewModel stores.
36+
ViewModelStoreOwnerProvider {
37+
ContentView()
38+
}
39+
.environmentObject(appContainer)
2840
}
2941
}
3042
}

Fruitties/shared/src/commonMain/kotlin/com/example/fruitties/viewmodel/CartViewModel.kt

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ package com.example.fruitties.viewmodel
1919
import androidx.lifecycle.ViewModel
2020
import androidx.lifecycle.ViewModelProvider
2121
import androidx.lifecycle.viewModelScope
22-
import androidx.lifecycle.viewmodel.CreationExtras
23-
import androidx.lifecycle.viewmodel.MutableCreationExtras
24-
import androidx.lifecycle.viewmodel.initializer
25-
import androidx.lifecycle.viewmodel.viewModelFactory
2622
import com.example.fruitties.DataRepository
27-
import com.example.fruitties.di.AppContainer
2823
import com.example.fruitties.model.CartItemDetails
2924
import kotlinx.coroutines.flow.SharingStarted
3025
import kotlinx.coroutines.flow.StateFlow
@@ -34,6 +29,16 @@ import kotlinx.coroutines.flow.stateIn
3429
class CartViewModel(
3530
private val repository: DataRepository,
3631
) : ViewModel() {
32+
33+
init {
34+
println("hello from CartViewmodel")
35+
}
36+
37+
override fun onCleared() {
38+
super.onCleared()
39+
println("clearing CartViewModel")
40+
}
41+
3742
val cartUiState: StateFlow<CartUiState> =
3843
repository.cartDetails
3944
.map { details ->
@@ -48,20 +53,9 @@ class CartViewModel(
4853
)
4954

5055
companion object {
51-
val APP_CONTAINER_KEY = CreationExtras.Key<AppContainer>()
52-
53-
val Factory: ViewModelProvider.Factory = viewModelFactory {
54-
initializer {
55-
val appContainer = this[APP_CONTAINER_KEY] as AppContainer
56-
val repository = appContainer.dataRepository
57-
CartViewModel(repository = repository)
58-
}
56+
val Factory: ViewModelProvider.Factory = vmFactory {
57+
CartViewModel(repository = it.dataRepository)
5958
}
60-
61-
fun creationExtras(appContainer: AppContainer): CreationExtras =
62-
MutableCreationExtras().apply {
63-
set(APP_CONTAINER_KEY, appContainer)
64-
}
6559
}
6660
}
6761

0 commit comments

Comments
 (0)