-
Notifications
You must be signed in to change notification settings - Fork 2
Home
KMPObservableBridge is a Swift-first, lifecycle-safe observation bridge for Kotlin Multiplatform ViewModels and SwiftUI.
It integrates with SKIE, KMP-NativeCoroutines, Combine publishers, callbacks, and custom asynchronous sources while preserving SwiftUI ownership, identity, lifetime, and dependency semantics.
- GitHub repository
- Installation and quick start
- Architecture
- DailyPulse example
- Benchmark methodology
- Latest release
- Issue tracker
KMPObservableBridge is designed around these principles:
- Kotlin remains the authoritative business-state owner.
- SwiftUI retains its native ownership and identity semantics.
- The bridge does not copy business state into a shadow Swift ViewModel.
- Macro-selected fields use compile-time-checked Swift key paths.
- No runtime reflection, Objective-C interception, or method swizzling is used.
- Macro-configured wrappers observing the same model share a static observation hub.
- Observation, cancellation, rebinding, and disposal follow deterministic lifetimes.
- SKIE and KMP-NativeCoroutines integrations remain isolated package products.
Add the package through Xcode’s Package Dependencies interface:
https://github.com/sonmbol/KMPObservableBridge.git
Choose the product matching your integration:
| Integration | Product |
|---|---|
| SKIE StateFlow | KMPObservableBridgeSKIE |
| KMP-NativeCoroutines NativeFlow | KMPObservableBridgeNative |
| Combine, callbacks, or custom adapters | KMPObservableBridge |
Import the generated Kotlin framework and the SKIE integration:
import shared
import KMPObservableBridgeSKIEEnable synchronous current-value projection once in the application module:
extension SkieSwiftStateFlow:
@retroactive KMPValueProperty {}Declare the observable fields for an imported Kotlin ViewModel:
@KMPObservable(
ArticleViewModel.self,
fields: \.articleState
)
extension ArticleViewModel:
@retroactive KMPStaticallyObservable {}Swift macros cannot currently enumerate every member of an imported Kotlin type, so observable fields must be selected explicitly.
This preserves compile-time validation and avoids runtime reflection.
Own a Kotlin ViewModel for one SwiftUI identity:
struct ArticleScreen: View {
@KMPStateObject
private var article = ArticleViewModel()
var body: some View {
ArticleContent(viewModel: article)
}
}Observe a ViewModel owned elsewhere:
struct ArticleContent: View {
@KMPObservedObject
private var article: ArticleViewModel
init(viewModel: ArticleViewModel) {
_article = KMPObservedObject(viewModel)
}
var body: some View {
Text(article.articleState.title)
}
}Propagate the same observation store through the SwiftUI environment:
ArticleContent()
.kmpEnvironmentObject($article)Consume it from a descendant:
struct ArticleContent: View {
@KMPEnvironmentObject
private var article: ArticleViewModel
}The wrapped property remains the original Kotlin ViewModel:
article.retry()
article.refresh()The projected store provides synchronous Swift values from containers conforming to KMPValueProperty:
let message: String = $article.messageState
let loading: Bool = $article.loadingStateA genuinely writable Kotlin export produces a SwiftUI binding:
TextField(
"Search",
text: $article.searchText
)Read-only StateFlows do not produce writable bindings. Update immutable Kotlin state through Kotlin actions.
On iOS 17, macOS 14, tvOS 17, watchOS 10, and later, projected key-path-backed fields receive independent Observation dependencies.
A view reading:
$article.articleStatedoes not depend on an unrelated projected field.
Direct access through the wrapped Kotlin model intentionally registers a global model dependency:
article.articleStateOn earlier supported systems, the bridge uses the correct global ObservableObject.objectWillChange fallback.
Import the dedicated integration product:
import shared
import KMPObservableBridgeNativeObserve the exported NativeFlow explicitly:
struct NativeCoroutinesExampleView: View {
@KMPStateObject(
state: \.kmpObservationFlow,
updatePolicy: .immediate
)
private var example = BridgeExampleViewModel()
var body: some View {
Text(example.nativeMessageValue)
}
}The NativeFlow supplies observation and cancellation. The separately exported Kotlin property supplies the current renderable value.
KMPObservableBridgeNative does not treat NativeFlow as a synchronous value container and does not expose SKIE symbols.
KMPStateObject owns its ViewModel for the lifetime of its SwiftUI identity.
An explicit disposer can be supplied:
@KMPStateObject(
wrappedValue: ProfileViewModel(),
dispose: { $0.clear() }
)
private var profileA model conforming to KMPDisposable can be disposed automatically by an owning store.
KMPObservedObject observes an externally owned model and never disposes it.
When an observed wrapper receives a different model:
- The previous observation is cancelled.
- The new model is stored.
- New observation begins.
- Existing dependencies are invalidated.
- Generation checks suppress stale emissions from the previous source.
Macro-configured wrappers observing the same Kotlin object share one static observation hub.
The final listener cancels the hub’s underlying observations.
Explicit adapters own the observation created for their wrapper.
Coalesced delivery is the default:
@KMPStateObject
private var article = ArticleViewModel()Same-turn changes are unioned and flushed once without losing independent field dependencies.
Event-sensitive consumers can request immediate delivery:
@KMPStateObject(updatePolicy: .immediate)
private var article = ArticleViewModel()The DailyPulse example demonstrates:
- SKIE StateFlow observation
- StateObject ownership
- ObservedObject children
- Environment propagation
- Writable Kotlin bindings
- KMP-NativeCoroutines
- Callback cancellation
- Combine publishers
- Injector-owned ViewModels
- Loading, error, empty, and populated states
- Dark-mode previews
Preview presentation views use ordinary Swift values and do not initialize Koin, allocate Kotlin ViewModels, start coroutines, collect flows, or perform network requests.
- Swift 5.9 and later
- iOS 15 and later
- macOS 11 and later
- tvOS 14 and later
- watchOS 7 and later
Toolchain-specific manifests support matching SwiftSyntax releases for Swift 5.9 through Swift 6.2.
We welcome alternative engineering proposals for two active investigations:
- Compile-time discovery of imported observable properties
- A more Swift-native value and Binding projection API
The existing implementation remains the tested baseline. Proposals should explain their impact on rendering, allocations, actor crossings, collectors, cancellation, source compatibility, and ARC/Kotlin GC lifetime.
Contributions are welcome in areas including:
- lifecycle and memory stress testing;
- real-device performance measurements;
- additional exporter integrations;
- SKIE and NativeCoroutines fixtures;
- SwiftUI hosting tests;
- macro research;
- documentation;
- and API design proposals.
Please include measurable evidence for performance-sensitive changes.
- Use GitHub Issues for reproducible bugs and feature requests.
- Use GitHub Discussions for integration questions and architecture proposals.
Built for Kotlin Multiplatform teams that want SwiftUI to remain SwiftUI.