Skip to content
Go back

🧩 Koin Injection on iOS Without Reified Crashes: A Clean KMM Pattern

by KMP Bits

KMP Bits Cover

Ever tried calling get<T>() from Swift in your KMP project and hit a mysterious UnsupportedOperationException: reified inline function can’t be called from Swift?

Yeah, same here. Let’s fix that once and for all: cleanly, safely, and without leaking half your Kotlin architecture into Swift.


🚨 The Problem

You’re building a Kotlin Multiplatform app with shared ViewModels managed by Koin. On Android, everything’s smooth:

val viewModel = get<SomeViewModel>()

But when you try the same from Swift…

let viewModel = KoinKt.get(SomeViewModel.self)

Boom 💥 — runtime crash:

UnsupportedOperationException: Reified inline functions can’t be called from Swift

That’s because reified generics rely on Kotlin’s compiler magic, which disappears once you cross into Swift/ObjC land.

From Swift, there’s no generic type information for Koin to resolve.

So what do many devs do?

They pass all constructor dependencies from Swift manually. It works, but it’s ugly and leaky:

We want a clean solution:


🧠 The Idea

Instead of calling get<T>() from Swift, we expose tiny Kotlin functions that internally use get { parametersOf(...) }.

Then, from Swift, we resolve them by calling a small Kotlin helper singleton that exposes safe functions for each ViewModel.

The result: Swift stays clean and type-safe, Koin stays in control.


🧩 Step 1: Define Your Koin Module

// commonMain
factory { (id: Long) ->
    SomeViewModelWithId(
        id = id,
        repository = get(),
    )
}

single { SomeViewModel(get(), get()) }
// …other ViewModels

🧰 Step 2: Create a Kotlin Helper for iOS

This helper exposes functions (not inject() properties) to avoid leaking Lazy<T> into Swift.

// iosMain or commonMain
package your.package.di

import org.koin.core.component.KoinComponent
import org.koin.core.parameter.parametersOf
import org.koin.core.component.get
import org.koin.core.component.inject

object DependencyHelper : KoinComponent {

    // Zero-parameter
    val someViewModel by inject<SomeViewModel>()

    // Parameterized
    fun someViewModelWithId(id: Long): SomeViewModelWithId =
        get { parametersOf(id) }
}

🧱 Step 3: Usage in SwiftUI

You create a thin Swift view model wrapper so SwiftUI can observe state changes, while the internal logic stays in the shared Kotlin ViewModel.

@MainActor
final class SomeViewModelWrapper: ObservableObject {
    private let viewModel: SomeViewModelWithId
    
    init(id: Int64) {
    	viewModel = DependencyHelper.someViewModelWithId(id: id)
    }
    
    // The rest of the functions, like observing the state, etc
}

Then, use it in SwiftUI, like you would with any other Swift view model.

struct DetailsView: View {
    @StateObject private var viewModel: SomeViewModelWrapper
    
    init(id: Int64) {
        _viewModel = StateObject(wrappedValue: SomeViewModelWrapper(id: id))
    }

    var body: some View {
        // Use viewModel safely
    }
}

That’s it. No more crashes. No more passing repositories around. Just clean, type-safe DI that works.


✨ Why This Approach Shines


🧭 Alternatives You Can Compare

ApproachProsCons
Helper functions (this article)Clean, type-safe, Swift-friendlyManual helper maintenance
Qualifiers / resolveByNameGeneric bridgeStrongly typed
Factory interfacesGreat for complex paramsSlightly more boilerplate
ScopesScoped lifecycleMore setup
ObjCClass / KClass bridgesWhen availableNot always supported

⚠️ Pitfalls & Pro Tips


🧩 Takeaway

This small pattern keeps your dependency graph fully Kotlin-first and your SwiftUI code clean and declarative.

KMP gives us shared power, but clarity is power too.

Keep DI in Kotlin, let Swift focus on building beautiful views.


💻 [Sample gist / helper code link here]



Share this post on:

Next Post
🔔 Cross-Platform Notifications with KMP — All in Kotlin!