A playground exploring the MVVM pattern with KMP ViewModel on iOS (SwiftUI & UIKit).
Important
This project is a Proof of Concept (PoC) and a collection of experiments. It is intended for exploration and learning, not as a strict production-ready guide.
Implementing MVVM in Kotlin Multiplatform for iOS can be tricky due to lifecycle management. While Android has first-class support for ViewModel, iOS requires workarounds to ensure proper memory management and lifecycle alignment.
This repository demonstrates various ways to bridge the gap between KMP and iOS.
- π Requirements
- ποΈ The ViewModel Pattern
- π± SwiftUI Integration
- ποΈ UIKit Support
- πΎ Database (Room)
- π Dependency Injection (Koin)
- π Logging & DataStore
Tip
Use explicitAPI in Gradle to control code visibility.
By default, all public Kotlin code is exported to the iOS framework. Reducing visibility (internal, private) leads to:
- β‘ Faster build times
- π Smaller binary size
- π Better Xcode autocomplete performance
To properly use KMP ViewModels on iOS, we need to handle the export and lifecycle.
In your shared Gradle file, you must export the androidx.lifecycle.viewmodel so it's accessible in Swift.
kotlin {
// ...
cocoapods { // or framework
export(libs.androidx.lifecycle.viewmodel)
}
sourceSets {
commonMain.dependencies {
api(libs.androidx.lifecycle.viewmodel)
}
}
}SKIE is used to bridge Kotlin Flows to Swift effortlessly.
skie {
features {
enableSwiftUIObservingPreview = true // For >= iOS 15
enableFutureCombineExtensionPreview = true
enableFlowCombineConvertorPreview = true
}
}We wrap the KMP ViewModel in an ObservableObject to align its lifecycle with SwiftUI views.
class SharedViewModel<VM: ViewModel>: ObservableObject {
private let key = String(describing: type(of: VM.self))
private let viewModelStore = ViewModelStore()
init(_ viewModel: VM = .init()) {
viewModelStore.put(key: key, viewModel: viewModel)
}
var instance: VM {
(viewModelStore.get(key: key) as? VM)!
}
deinit {
viewModelStore.clear() // Triggers onCleared() in Kotlin
}
}The MainScreenViewModel provides a common logic for both platforms.
- Android: Direct integration using standard
viewModel()delegate. Example. - iOS: Uses SKIE to transform Kotlin Flows into Swift async/await or Combine.
Uses SKIE's SwiftUI observing which leverages the .task modifier.
π Example Code
A custom implementation using .onAppear for older iOS versions.
π Example Code
Uses a custom Swift Macro to automatically wrap KMP ViewModels. π Example Code
Standard SwiftUI ObservableObject using KMP services via Koin, without using KMP ViewModel.
π Example Code
UIKit is fully supported by combining SharedViewModel with SKIE Combine extensions.
π Example Code
Example implementation using Room KMP. π Database Module
Important
Avoid exporting generated database code to iOS. Use a parent module to hide the database implementation details and only export necessary interfaces. Large schemas can significantly impact build times and binary size.
This project uses Koin with Annotations.
To use Koin ViewModels in a shared module without Compose, add this helper: π KMPViewModelAnnotation.kt
- Export Koin Helpers: AppInit.ios.kt
- Setup Swift Context:
// Store somewhere inside a singleton/static your Kotlin's KoinApplication instance for later usage AppContext.shared.koinApplication = koinApp // Your initialized Koin application
- Use Swift Helpers: KoinHelper.swift
// Equivalent of 'get()' koin's methos
let accountService: AccountService = koinGet()
// Equivalent of 'by inject()' koin method
lazy var accountService: AccountService = koinGet()
// With parameters
let logger: KermitLogger = koinGet(parameters: ["MainScreen"])
// ViewModel with parameters
@StateObject private var viewModel = StateObject(wrappedValue: SharedViewModel(koinGet(parameters: ["ID"])))- Logging: Powered by Kermit.
- Persistence: Jetpack DataStore for multiplatform key-value storage.