From 5999f7a9f286aeb90678cf699fc6ef0918d5bc0b Mon Sep 17 00:00:00 2001 From: sonmbol Date: Tue, 28 Jul 2026 13:02:46 +0400 Subject: [PATCH] docs: strengthen project adoption and discovery --- ADOPTION.md | 38 ++- CODE_OF_CONDUCT.md | 45 ++++ CONTRIBUTING.md | 46 +++- Docs/Choosing-SKIE-or-KMP-NativeCoroutines.md | 178 +++++++------- Docs/Evaluating-KMPObservableBridge.md | 134 +++++++++++ LAUNCH_KIT.md | 223 +++++++++--------- README.md | 14 +- 7 files changed, 474 insertions(+), 204 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Docs/Evaluating-KMPObservableBridge.md diff --git a/ADOPTION.md b/ADOPTION.md index 70bee53..c8efa9c 100644 --- a/ADOPTION.md +++ b/ADOPTION.md @@ -1,5 +1,8 @@ # Adoption Playbook +The objective is not a short-lived star count. It is a measurable path from a +repository visit to a correct integration that teams keep after evaluating it. + ## First adopters Offer hands-on help integrating one non-critical screen. Ask each team for: @@ -16,6 +19,17 @@ Suggested message: > with the integration and lifecycle review. In return, I’d value candid API > feedback; public attribution is entirely optional. +Choose pilot screens that: + +- Are non-critical and can be removed without a migration. +- Have one clearly owned ViewModel and one or two observable fields. +- Already use SKIE or KMP-NativeCoroutines successfully. +- Have tests or a deterministic interaction that exposes lifecycle mistakes. + +For each pilot, record the time required to install, configure, render the first +value, and verify cancellation. Turn repeated confusion into documentation or a +compile-time diagnostic. + ## Where to ask - Kotlin Slack `#multiplatform` @@ -39,16 +53,32 @@ For every consenting adopter, record: Never imply production use without confirmation from the application owner. -## Success measures +Before publishing a name, logo, quotation, benchmark, or architecture detail, +obtain explicit written permission for that specific material. An anonymous +technical case study is a valid alternative. + +## Adoption funnel Review monthly: -- README visits to package-resolution or clone activity -- Swift Package Index builds +- Unique repository visitors and their referral sources +- Clones and package-resolution activity - Unique cloners and dependent repositories - Discussions that reach a working integration +- Evaluations that produce a rendered screen +- Evaluations that remain after one release cycle - Time to first maintainer response -- Retention of known adopters after one release cycle Stars are useful discovery signals, but successful integrations and retained users are the primary measures. + +## Evidence worth publishing + +- A reproducible before/after adapter diff. +- Notification and body-evaluation counts for a real screen. +- Collector counts with parent, child, and environment wrappers. +- Cancellation behavior after navigation and model rebinding. +- Toolchain and framework versions for every benchmark. +- Limitations discovered during adoption and how they were resolved. + +Never publish a synthetic benchmark as proof of application-level performance. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c009baf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,45 @@ +# Code of Conduct + +## Our commitment + +KMPObservableBridge is committed to a welcoming, respectful, and technically +constructive community for people of every background and experience level. + +Participation includes repository activity, issues, pull requests, +discussions, reviews, community events, and project-related communication. + +## Expected behavior + +- Discuss ideas and evidence rather than attacking people. +- Assume good intent while remaining precise about technical tradeoffs. +- Welcome questions and explain unfamiliar Swift, SwiftUI, Kotlin, and + interoperability concepts without condescension. +- Give actionable review feedback with enough context to help. +- Disclose conflicts of interest when recommending competing or affiliated + projects. +- Respect privacy and never publish private integration details without + permission. + +## Unacceptable behavior + +- Harassment, discrimination, threats, or personal insults. +- Sexualized language or unwanted attention. +- Trolling, deliberate disruption, or repeated bad-faith argument. +- Publishing another person's private information. +- Pressuring adopters to provide public endorsements. +- Manipulating benchmarks, adoption claims, or project comparisons. + +## Enforcement + +Report conduct concerns privately to the repository owner. Do not open a public +issue containing personal or sensitive information. Reports will be reviewed +promptly and handled with appropriate confidentiality. + +Maintainers may edit or remove contributions, comments, or other participation +that violates this policy. Repeated or serious violations may result in a +temporary or permanent ban from project spaces. + +## Scope + +This policy applies whenever someone represents the project or participates in +an official KMPObservableBridge space. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0cac961..47285dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,47 @@ # Contributing -1. Open an issue for public API or behavior changes. -2. Keep the core free of Kotlin framework and third-party dependencies. -3. Add tests for lifecycle, actor isolation, cancellation, and deallocation. -4. Run: +Thank you for helping make Kotlin state feel native in SwiftUI. Bug reports, +API critiques, documentation fixes, integration fixtures, and performance +evidence are all useful. + +## Before changing code + +1. Search the [issues](https://github.com/sonmbol/KMPObservableBridge/issues) + and [discussions](https://github.com/sonmbol/KMPObservableBridge/discussions). +2. Open an issue before changing public API or observable behavior. +3. Keep `KMPObservableBridge` independent of Kotlin frameworks and third-party + runtimes. Exporter-specific behavior belongs in its integration target. +4. Preserve existing consumer source compatibility unless the issue explicitly + targets a major release. + +Good first contributions include clearer diagnostics, DocC examples, example +accessibility, and additional integration fixtures. Changes to invalidation, +ownership, cancellation, macros, or actor isolation require focused tests. + +## Validation + +Run: ```shell -swift test swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors +./Scripts/check-api.sh +./Scripts/check-package-manifests.sh git diff --check ``` -Public APIs must include documentation and migration notes. Performance claims -must include a reproducible benchmark. +When changing the example integration, also build DailyPulse using the procedure +in [`Examples/DailyPulse/iosApp/README.md`](Examples/DailyPulse/iosApp/README.md). + +## Pull requests + +- Keep the change focused and explain its rendering and lifetime effects. +- Add regression tests for lifecycle, actor isolation, cancellation, rebinding, + dependency selection, and deallocation where applicable. +- Document public APIs and include migration notes for behavior changes. +- Include toolchain, configuration, commands, and raw measurements with every + performance claim. +- Never include private application code, credentials, or adopter information + without permission. + +All participation is governed by the +[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). diff --git a/Docs/Choosing-SKIE-or-KMP-NativeCoroutines.md b/Docs/Choosing-SKIE-or-KMP-NativeCoroutines.md index 2585f12..c02408d 100644 --- a/Docs/Choosing-SKIE-or-KMP-NativeCoroutines.md +++ b/Docs/Choosing-SKIE-or-KMP-NativeCoroutines.md @@ -1,135 +1,143 @@ # Choosing SKIE or KMP-NativeCoroutines for SwiftUI -SKIE and KMP-NativeCoroutines both make Kotlin coroutine APIs easier to consume -from Swift, but they offer different integration shapes. KMPObservableBridge -supports both without forcing either dependency into the Swift package. +SKIE and KMP-NativeCoroutines expose Kotlin coroutine APIs to Swift in different +shapes. KMPObservableBridge isolates their APIs in separate package products so +an application can choose its existing export strategy without leaking the +other integration into source or binaries. ## Quick comparison | Question | SKIE | KMP-NativeCoroutines | | --- | --- | --- | -| Typical Swift shape | `StateFlow` as an async sequence with value access | Generated `NativeFlow` callback or async wrappers | -| Shortest bridge syntax | Automatic lazy discovery | `KMPNativeObservable` canonical flow | -| Runtime getter interception | Used by automatic mode | No | -| Explicit typed path available | Yes | Yes | -| Best fit | Teams already standardizing on SKIE | Teams already generating NativeCoroutines APIs | - -Neither choice is universally better. Prefer the tool already present in the +| Typical Swift shape | Typed `AsyncSequence` with synchronous StateFlow value access | Generated `NativeFlow` callback or async wrapper | +| Bridge product | `KMPObservableBridgeSKIE` | `KMPObservableBridgeNative` | +| Configuration | Explicit macro fields or wrapper key paths | Explicit NativeFlow key path | +| Field-level dependency metadata | Yes, for macro-configured StateFlows | Global unless a custom keyed adapter provides metadata | +| Runtime reflection | None | None | +| Best fit | Teams already using SKIE | Teams already generating NativeCoroutines APIs | + +Neither exporter is universally better. Prefer the one already validated in the application unless a measured limitation justifies migration. -## SKIE: shortest SwiftUI declaration +## SKIE -For compatible generated frameworks: +Declare current-value interoperability once in the application module: ```swift -@KMPStateObject -private var profile = ProfileViewModel() -``` - -KMPObservableBridge lazily inspects naturally accessed Kotlin getters. When a -returned value conforms to that framework's exported StateFlow protocol, the -bridge starts the matching SKIE iterator. +import shared +import KMPObservableBridgeSKIE -This removes per-property registration, but it relies on generated runtime -details. Debug builds log unavailable compatibility, successful getter -discovery, and incompatible iterator method shapes. +extension SkieSwiftStateFlow: @retroactive KMPValueProperty {} +``` -Use an explicit key path when deterministic observation is more important than -the shortest declaration: +Then declare the StateFlows that form a ViewModel’s observation plan: ```swift -@KMPStateObject( - wrappedValue: ProfileViewModel(), - state: \.state +@KMPObservable( + ProfileViewModel.self, + fields: \.profileState, \.permissionsState ) -private var profile +extension ProfileViewModel: @retroactive KMPStaticallyObservable {} ``` -## KMP-NativeCoroutines: explicit generated contract +The macro expands these compiler-checked key paths into the static observation +plan. Swift macros cannot inspect members of an imported Kotlin type, so fields +are intentionally explicit. The bridge does not use Objective-C discovery, +getter interception, or generated application source. -A model can expose one NativeFlow representing its canonical invalidation -stream: +Own or observe the model normally: ```swift -extension ProfileViewModel: KMPNativeObservable { - public var kmpObservationFlow: KMPNativeFlow< - ProfileState, - Error, - KotlinUnit - > { - profileStateFlow - } -} +@KMPStateObject +private var profile = ProfileViewModel() + +Text($profile.profileState.title) ``` -Then the same wrapper syntax selects NativeFlow before considering SKIE: +Use an explicit wrapper adapter when a conformance should remain local to one +use site: ```swift -@KMPStateObject +@KMPStateObject(state: \.profileState) private var profile = ProfileViewModel() ``` -This route performs no Objective-C method interception or SKIE iterator lookup. -Each emission enters the common main-actor, failure-policy, cancellation, and -coalescing store. +## KMP-NativeCoroutines + +Link `KMPObservableBridgeNative` and observe the generated NativeFlow explicitly: + +```swift +import shared +import KMPObservableBridgeNative + +@KMPStateObject( + state: \.kmpObservationFlow, + updatePolicy: .immediate +) +private var example = BridgeExampleViewModel() +``` + +The NativeFlow is the notification and cancellation source. Render the +separately exported current property: + +```swift +Text(example.nativeMessageValue) +``` -The exact exported generic types depend on the generated framework. Follow the -generated NativeFlow signature rather than copying placeholder names from an -example. +The exact generated property and generic types depend on the Kotlin declaration +and NativeCoroutines version. Use the generated Swift interface as the contract. -## Performance considerations +## Performance and rendering behavior -For SKIE automatic discovery: +For macro-configured SKIE: -- compatible and incompatible runtime descriptors are cached per model class; -- eligible getters perform a small return-value protocol check; -- repeated reads of the same StateFlow identity are deduplicated; -- one active iterator is kept per discovered getter and model. +- One static hub shares the model’s configured collectors across wrappers. +- Consecutive equal StateFlow values are suppressed. +- Projected field reads can register field-level dependencies on + Observation-capable platforms. +- Direct model reads retain global invalidation semantics. For NativeFlow: -- there is no getter interception; -- the primary cost is the Kotlin-to-Swift callback for each emission; -- cancellation propagates through the returned NativeFlow cancellation handle. +- The Kotlin-to-Swift callback crosses to `MainActor` once. +- Cancellation propagates through the generated cancellation handle. +- The adapter invalidates globally unless it supplies a known dependency key. -For both routes, SwiftUI rendering usually costs more than bridge dispatch. -The default `.coalesced` policy batches bursts arriving in one main-actor turn. -Use `.immediate` only when every emission must produce a distinct invalidation. +For both routes, `.coalesced` unions changes in one main-actor turn. +`.immediate` preserves every accepted emission when that semantic is required. +Neither route duplicates Kotlin business state in Swift. ## Selection guide -Choose automatic SKIE when: +Choose SKIE when: - the application already uses and tests SKIE; -- minimum declaration ceremony is important; -- the Kotlin/SKIE version combination is pinned in CI; -- lazy observation matches the view's access pattern. - -Choose explicit SKIE key paths when: - -- observation topology must be obvious in code; -- only selected flows should invalidate a screen; -- maximum compile-time guidance is preferred. +- synchronous StateFlow value access is useful to rendering; +- explicit macro fields fit the project’s feature organization; +- field-aware Observation is valuable. Choose KMP-NativeCoroutines when: -- it is already the application's coroutine export strategy; -- a canonical NativeFlow can represent screen invalidation; -- avoiding runtime getter interception is a priority. +- it is already the application’s coroutine export strategy; +- a generated NativeFlow provides the desired invalidation contract; +- state is exposed separately through a current-value property. -Choose a custom adapter when the application already has a stable callback, -Combine, or platform-specific observation contract. +Choose callbacks, Combine, or a custom adapter when the application already has +a stable platform-specific contract that is easier to validate than migrating +exporters. -## Validate the decision +## Validate either decision -Whichever route you choose, test: +Test: -1. initial state replay; -2. a later Kotlin emission; -3. background delivery to the main actor; -4. model identity replacement; -5. cancellation when the view/store disappears; -6. ViewModel disposal only by its owner. +1. Initial value rendering. +2. A later Kotlin emission. +3. Foreign-thread delivery to `MainActor`. +4. Model identity replacement and stale-emission suppression. +5. Cancellation when the wrapper’s identity ends. +6. Disposal by owners only. +7. Multiple wrappers and their expected collector count. +8. Equal emissions and body-evaluation counts. -The included DailyPulse application builds a generated Kotlin framework with -both SKIE and KMP-NativeCoroutines examples. +The DailyPulse example builds a real Kotlin framework and demonstrates both +integration products. diff --git a/Docs/Evaluating-KMPObservableBridge.md b/Docs/Evaluating-KMPObservableBridge.md new file mode 100644 index 0000000..8e014de --- /dev/null +++ b/Docs/Evaluating-KMPObservableBridge.md @@ -0,0 +1,134 @@ +# Evaluating KMPObservableBridge + +This guide helps a team decide whether KMPObservableBridge fits its KMP and +SwiftUI architecture. It is a decision guide, not a claim that one integration +style is universally best. + +## Choose this bridge when + +- Kotlin ViewModels intentionally own state consumed by SwiftUI. +- The iOS team wants explicit SwiftUI-style ownership. +- Kotlin must remain the single source of truth. +- SKIE, KMP-NativeCoroutines, callbacks, Combine, or mixed adapters are in use. +- Field-level dependencies on Observation-capable platforms matter. +- The project wants compiler-checked key paths instead of runtime discovery. +- Ownership, rebinding, cancellation, and disposal need one consistent model. + +## Prefer ordinary SwiftUI state when + +- KMP shares only domain, networking, persistence, or use-case code. +- Presentation state is intentionally platform-specific. +- A Swift ViewModel already owns the screen's state and lifetime. +- The application does not export asynchronous Kotlin state to iOS. + +Adding a bridge where there is no cross-language presentation-state boundary +creates complexity without a corresponding benefit. + +## Architectural comparison + +| Question | KMPObservableBridge | Kotlin-integrated ViewModel bridge | Manual Swift adapter | +| --- | --- | --- | --- | +| Kotlin dependency | None required by the core bridge | Usually requires Kotlin-side library APIs | None | +| SwiftUI ownership | Explicit owning, observed, and environment wrappers | Defined by that library's integration | Defined by each adapter | +| State authority | Kotlin | Kotlin | Often duplicated in Swift | +| Discovery | Explicit compile-time Swift key paths | Kotlin-side primitives or metadata | Hand-written subscriptions | +| Reflection | None | Implementation-dependent | None required | +| Collector sharing | Static macro plans share per model identity | Implementation-dependent | Must be designed per adapter | +| Field dependencies | Projected keyed fields on modern Observation platforms | Depends on Kotlin/Swift registrar integration | Depends on adapter design | +| Exporters | SKIE, NativeCoroutines, callbacks, Combine, custom | Usually a selected ecosystem | Any source handled manually | +| Swift boilerplate | One observation declaration per imported model | Kotlin integration plus Swift wrappers | One adapter ViewModel per screen or feature | + +The relevant comparison is not the number of API types. Evaluate who owns +state, how dependencies are registered, when collectors start and stop, and +whether the architecture adds another business-state copy. + +## Important distinctions + +### SKIE + +SKIE StateFlow wrappers provide both asynchronous iteration and synchronous +current-value access. `KMPValueProperty` enables projected reads through the +bridge store: + +```swift +let message: String = $profile.messageState +``` + +### KMP-NativeCoroutines + +NativeFlow is an observation and cancellation source, not a synchronous value +container. Observe the flow explicitly and read the separately exported +current value: + +```swift +@KMPStateObject( + state: \.kmpObservationFlow, + updatePolicy: .immediate +) +private var profile = ProfileViewModel() + +Text(profile.nativeMessageValue) +``` + +### Direct and projected reads + +`profile.state` reads the original Kotlin model and registers a global model +dependency. `$profile.state` passes through the projected store and can +register a field dependency when the export conforms to `KMPValueProperty`. + +## Low-risk evaluation + +Start with one non-critical screen: + +1. Select one Kotlin ViewModel and one exported state stream. +2. Declare the state with `@KMPObservable` or configure it explicitly at the + property wrapper. +3. Keep the bridge in a thin live container. +4. Pass Swift values, bindings, and action closures to a pure SwiftUI + presentation view. +5. Verify construction count, collector count, cancellation, rebinding, and + disposal in the real navigation hierarchy. +6. Profile body evaluation and allocations before expanding adoption. + +## Migration from a manual Swift adapter + +Given an adapter that copies Kotlin state: + +```swift +final class ProfileAdapter: ObservableObject { + @Published var state: ProfileState + let model: ProfileViewModel +} +``` + +migrate incrementally: + +1. Keep the existing presentation view unchanged. +2. Replace the adapter owner with `KMPStateObject`. +3. Feed the presentation view from `$profile.profileState`. +4. Forward actions directly from the Kotlin model. +5. Remove copied Swift state only after lifecycle and rendering tests pass. + +The target boundary becomes: + +```text +Kotlin ViewModel + ↓ thin observation container +Swift values + Binding + action closures + ↓ +Pure SwiftUI presentation +``` + +## Evidence to collect + +- Kotlin ViewModel construction and disposal counts. +- Underlying collector counts with parent, child, and environment consumers. +- SwiftUI body evaluations for independent fields. +- Cancellation behavior after navigation and model replacement. +- Weak-reference release across Swift ARC and Kotlin GC boundaries. +- Immediate and coalesced delivery measurements. + +Share results in +[GitHub Discussions](https://github.com/sonmbol/KMPObservableBridge/discussions) +or report a reproducible problem through +[GitHub Issues](https://github.com/sonmbol/KMPObservableBridge/issues). diff --git a/LAUNCH_KIT.md b/LAUNCH_KIT.md index d95ce60..c119151 100644 --- a/LAUNCH_KIT.md +++ b/LAUNCH_KIT.md @@ -1,157 +1,170 @@ # KMPObservableBridge Launch Kit -Attach `Assets/social-preview.png` when announcing the release. +Use [`Assets/social-preview.png`](Assets/social-preview.png) for link previews +and [`Assets/demo.gif`](Assets/demo.gif) when the channel supports animation. +Keep every announcement technical, specific, and open to critical feedback. -## Demo video storyboard +## Core message -Record one 20–40 second clip in a 16:9 layout: +KMPObservableBridge 1.1.0 lets SwiftUI observe Kotlin Multiplatform ViewModels +without duplicating business state in a Swift adapter. Kotlin remains the +source of truth, while SwiftUI gets explicit ownership, lifecycle-safe +collection, field-level Observation on supported platforms, and native +bindings for writable exports. -1. Show a Kotlin `StateFlow` changing in the DailyPulse ViewModel. -2. Show the SwiftUI screen updating in the simulator. -3. Show the removed Swift adapter boilerplate. -4. End on: +The package: - ```swift - @KMPStateObject - private var profile = ProfileViewModel() - ``` +- Supports SKIE, KMP-NativeCoroutines, callbacks, Combine, and custom adapters. +- Requires no Kotlin superclass or runtime dependency in the Swift package. +- Uses explicit, compiler-checked macro fields instead of runtime reflection. +- Shares macro-configured collectors across wrappers for the same model. +- Supports iOS 15, macOS 11, tvOS 14, watchOS 7, and Swift 5.9 or later. -Use the caption: **Kotlin State. Native SwiftUI. No duplicate adapter -ViewModel.** +Repository: https://github.com/sonmbol/KMPObservableBridge -Export an MP4 for LinkedIn, X, Reddit, and Discord, plus a short GIF for the -README and GitHub Release. +Release: https://github.com/sonmbol/KMPObservableBridge/releases/tag/1.1.0 -## Short post +## Short launch post -KMPObservableBridge 1.0.1 is available: a lightweight, architecture-neutral way -to observe Kotlin Multiplatform state directly from SwiftUI. +KMPObservableBridge 1.1.0 is available. -It supports SKIE, KMP-NativeCoroutines, callbacks, Combine, and custom adapters -without requiring a Kotlin superclass, annotation, compiler plugin, generated -registration, or third-party runtime dependency in the Swift package. -It includes SwiftUI-style owned, observed, environment, and child ViewModels, -deterministic cancellation, rebinding, and coalesced updates. +It connects Kotlin Multiplatform ViewModels to SwiftUI without a duplicate +Swift adapter or a Kotlin-side framework dependency. Kotlin stays authoritative; +SwiftUI gets familiar ownership wrappers, deterministic cancellation, rebinding, +and field-level dependency tracking for explicitly selected SKIE StateFlows. -https://github.com/sonmbol/KMPObservableBridge +It also supports KMP-NativeCoroutines, callbacks, Combine, and custom adapters. +The repository includes a real Gradle → SKIE → Xcode example, benchmarks, and +strict-concurrency tests. -#KotlinMultiplatform #KMP #SwiftUI #iOSDev #Kotlin +I’m looking for two teams willing to evaluate it on one non-critical screen. +Critical feedback is welcome: -## Reddit +https://github.com/sonmbol/KMPObservableBridge -**Title:** KMPObservableBridge 1.0.1 — observe Kotlin state directly in SwiftUI +#KotlinMultiplatform #KMP #SwiftUI #iOSDev #Kotlin -I released KMPObservableBridge, a dependency-light Swift package for connecting -Kotlin Multiplatform state to SwiftUI without duplicating state in a Swift -adapter ViewModel. +## LinkedIn / X -The goal is architecture neutrality: no required Kotlin base class, annotation, -compiler plugin, or runtime. The same ownership model works with SKIE, -KMP-NativeCoroutines, generated async sequences, callbacks, Combine, and custom -adapters. +Using a Kotlin Multiplatform ViewModel in SwiftUI should not require duplicating +its state in a Swift adapter. -The 1.0 API mirrors SwiftUI concepts with `KMPStateObject`, -`KMPObservedObject`, `KMPEnvironmentObject`, and `KMPChildObject`. It handles -cancellation, model rebinding, stale-emission suppression, background -callbacks, observation failures, and hot-stream update coalescing. +```swift +@KMPObservable( + ArticleViewModel.self, + fields: \.articleState +) +extension ArticleViewModel: @retroactive KMPStaticallyObservable {} -Repository, documentation, examples, and tests: -https://github.com/sonmbol/KMPObservableBridge +@KMPStateObject +private var viewModel = ArticleViewModel() +``` -How does your project currently connect StateFlow to SwiftUI: a manual adapter, -SKIE, KMP-NativeCoroutines, or something else? I would especially value -feedback on API ergonomics, Swift concurrency behavior, and real-world KMP -framework integrations. +Kotlin remains the source of truth. SwiftUI receives explicit ownership, +field-level dependency tracking, deterministic cancellation, and native +bindings for writable exports. -## Kotlin Slack +KMPObservableBridge 1.1.0 supports SKIE, KMP-NativeCoroutines, callbacks, +Combine, and custom adapters: -I released KMPObservableBridge 1.0, a SwiftUI bridge for KMP state that works -with SKIE, KMP-NativeCoroutines, callbacks, Combine, and custom adapters without -a Kotlin-side dependency or required ViewModel base class. It follows -SwiftUI-style ownership and includes environment/child ViewModels, -deterministic cancellation and rebinding, failure policies, and coalesced -updates. Feedback is welcome: https://github.com/sonmbol/KMPObservableBridge -## Swift Forums +How are you connecting StateFlow to SwiftUI today? -**Title:** KMPObservableBridge: architecture-neutral Kotlin state observation for SwiftUI +#KotlinMultiplatform #SwiftUI #KMP #iOSDev -KMPObservableBridge lets SwiftUI observe state exposed by Kotlin Multiplatform -models while Kotlin remains the source of truth. Its API models SwiftUI -ownership instead of introducing a framework-owned Kotlin ViewModel hierarchy. +## Reddit -The package supports async sequences, callbacks, Combine publishers, and custom -adapters, with optional convenience for SKIE and KMP-NativeCoroutines. The core -depends only on Apple system frameworks and supports iOS 14, macOS 11, tvOS 14, -and watchOS 7. +**Title:** KMPObservableBridge 1.1.0 — field-aware Kotlin state observation for SwiftUI -The implementation focuses on actor-safe UI invalidation, deterministic -cancellation and disposal, identity-aware rebinding, stale-emission -suppression, and coalescing for hot streams. Review from developers experienced -with Swift concurrency and cross-language ownership is very welcome. +I built KMPObservableBridge to remove the duplicate Swift adapter ViewModel that +often appears between Kotlin state and SwiftUI. -https://github.com/sonmbol/KMPObservableBridge +The bridge keeps Kotlin as the only business-state store. For SKIE, an attached +macro creates a compile-time-checked observation plan from explicit StateFlow +key paths. On Observation-capable platforms, projected field reads can track a +specific field; direct model reads retain safe global invalidation behavior. +Older OS versions fall back to `ObservableObject`. -## LinkedIn / X +The package also supports KMP-NativeCoroutines, callbacks, Combine, and custom +adapters. Ownership is explicit through SwiftUI-style wrappers, collectors are +cancelled deterministically, stale emissions are suppressed after rebinding, +and macro-configured wrappers share collection for the same model. -Using Kotlin Multiplatform ViewModels in SwiftUI often creates a second Swift -adapter ViewModel for every screen. +There is no Kotlin base class, runtime reflection, or duplicated Swift value +store. The repository includes tests, benchmarks, DocC, and a real +Gradle → SKIE → Xcode example. -KMPObservableBridge removes that layer: +Repository and demo: +https://github.com/sonmbol/KMPObservableBridge -```swift -@KMPStateObject -private var profile = ProfileViewModel() -``` +I would value critical feedback on the API, Observation semantics, and +cross-language lifecycle model. I’m also offering hands-on help to two teams +that want to evaluate one non-critical screen. -Kotlin remains the source of truth. SwiftUI keeps native ownership, -invalidation, cancellation, rebinding, and lifecycle behavior. It supports -SKIE, KMP-NativeCoroutines, explicit async sequences, callbacks, and Combine. +## Kotlin Slack / community Discord -How are you connecting StateFlow to SwiftUI today? +I released KMPObservableBridge 1.1.0, a SwiftUI bridge for KMP ViewModels that +keeps Kotlin state authoritative. It supports SKIE with explicit, +compiler-checked fields, plus KMP-NativeCoroutines, callbacks, Combine, and +custom adapters. There is no required Kotlin superclass or Swift-side state +copy. The project focuses on SwiftUI ownership, field-aware invalidation, +deterministic cancellation, and safe rebinding. +I’m looking for two teams to evaluate one non-critical screen, and I’m happy to +help with the lifecycle review: https://github.com/sonmbol/KMPObservableBridge -#KotlinMultiplatform #KMP #SwiftUI #iOSDev +## Swift Forums -## Directory submission +**Title:** KMPObservableBridge: field-aware Kotlin state observation for SwiftUI -**Name:** KMPObservableBridge +KMPObservableBridge lets SwiftUI observe exported Kotlin Multiplatform models +while Kotlin remains the authoritative state store. -**URL:** https://github.com/sonmbol/KMPObservableBridge +The core design uses SwiftUI-style ownership wrappers and explicit observation +adapters. The SKIE integration uses an attached macro with compiler-checked +StateFlow key paths. Projected field reads participate in field-level +Observation on supported Apple platforms, while direct model access remains a +safe global dependency. iOS 15/16 retain `ObservableObject` invalidation. -**Description:** A lightweight, architecture-neutral SwiftUI observation bridge -for Kotlin Multiplatform state. Supports StateFlow through SKIE or -KMP-NativeCoroutines, generated async sequences, callbacks, Combine, and custom -adapters without a required Kotlin superclass, annotation, plugin, or runtime. +The package also supports KMP-NativeCoroutines, callbacks, Combine, and custom +adapters. It does not require a Kotlin base class, Objective-C reflection, or a +duplicated Swift value cache. -**Keywords:** Kotlin Multiplatform, KMP, KMM, SwiftUI, StateFlow, coroutines, -Kotlin/Native, SKIE, KMP-NativeCoroutines, Observation, MVVM, MVI +Review from developers experienced with Swift Observation, concurrency, +Kotlin/Native ownership, and macro APIs would be especially useful: +https://github.com/sonmbol/KMPObservableBridge ## Newsletter pitch -**Subject:** Project submission: KMPObservableBridge 1.0.1 +**Subject:** Project submission: KMPObservableBridge 1.1.0 -KMPObservableBridge is a Swift package that lets SwiftUI observe real Kotlin -Multiplatform ViewModels without a duplicate Swift adapter per screen. It -supports automatic SKIE StateFlow discovery, KMP-NativeCoroutines, typed async -sequence key paths, callbacks, and Combine while keeping lifecycle ownership -explicit. The repository includes strict-concurrency tests, Apple-platform CI, -DocC, benchmarks, and a generated Kotlin/SKIE example application. +KMPObservableBridge is a Swift package for connecting Kotlin Multiplatform +ViewModels to SwiftUI without duplicating business state in a Swift adapter. +It provides SwiftUI-style ownership, explicit compiler-checked SKIE fields, +field-level Observation on supported platforms, deterministic cancellation, +and safe model rebinding. Separate integrations support SKIE and +KMP-NativeCoroutines, while the core also accepts callbacks, Combine, and +custom adapters. The repository includes a real Gradle → SKIE → Xcode example, +strict-concurrency tests, benchmarks, DocC, and an architectural evaluation +guide. Repository: https://github.com/sonmbol/KMPObservableBridge -Release: https://github.com/sonmbol/KMPObservableBridge/releases/tag/1.0.1 +Release: https://github.com/sonmbol/KMPObservableBridge/releases/tag/1.1.0 + +## Thirty-day publishing plan -## Seven-day publishing schedule +| Timing | Channel | Goal | +| --- | --- | --- | +| Day 1 | GitHub Discussion | Explain 1.1.0 and invite technical review | +| Day 2–3 | Kotlin Slack / KMP Discord | Ask how teams currently bridge StateFlow | +| Day 4–5 | Reddit | Share the architecture and demo; request criticism | +| Week 2 | Swift Forums | Seek Observation and concurrency review | +| Week 2 | Five personalized maintainer messages | Recruit two pilot integrations | +| Week 3 | Issue or Discussion follow-ups | Publish answers and integration findings | +| Week 4 | GitHub traffic and adopter review | Refine onboarding using measured drop-off | -| Day | Channel | Asset | Goal | -| --- | --- | --- | --- | -| 1 | GitHub Release | Release notes + demo | Give visitors a stable landing page | -| 2 | Kotlin Slack / KMP Discord | Short post | Ask about existing StateFlow patterns | -| 3 | Reddit | Technical post + demo | Invite API and integration feedback | -| 4 | GitHub Discussions | Integration thread | Help one real ViewModel integration | -| 5 | Swift Forums | Architecture post | Reach Swift concurrency reviewers | -| 6 | Five maintainers | Personalized message | Recruit first adopters | -| 7 | GitHub Traffic | Referrals and clones | Refine the best-performing message | +Do not send bulk unsolicited messages, manufacture testimonials, or claim +production adoption without the application owner’s explicit permission. diff --git a/README.md b/README.md index 66d67db..c9197c6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ViewModels, SKIE, KMP-NativeCoroutines, and SwiftUI. [![Swift 5.9+](https://img.shields.io/badge/Swift-5.9%2B-F05138?logo=swift&logoColor=white)](https://swift.org) [![Platforms](https://img.shields.io/badge/Platforms-iOS%2015%2B%20%7C%20macOS%2011%2B-blue)](#requirements) -[![CI](https://github.com/sonmbol/KMPObservableBridge/actions/workflows/ci.yml/badge.svg)](https://github.com/sonmbol/KMPObservableBridge/actions/workflows/ci.yml) +[![CI](https://github.com/sonmbol/KMPObservableBridge/actions/workflows/swift.yml/badge.svg)](https://github.com/sonmbol/KMPObservableBridge/actions/workflows/swift.yml) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) @@ -31,6 +31,10 @@ var body: some View { No shadow Swift ViewModel. No copied business state. No runtime reflection, swizzling, selector discovery, or generated source file. +**Evaluate it in five minutes:** [install the package](#quick-start), explore the +[DailyPulse app](Examples/DailyPulse/iosApp), or read +[when to choose this bridge](Docs/Evaluating-KMPObservableBridge.md). + ## Why KMPObservableBridge? Kotlin remains the authoritative source of truth while SwiftUI receives native @@ -40,7 +44,7 @@ values, bindings, ownership semantics, and precise dependencies. | --- | --- | | Native ownership | `@KMPStateObject`, `@KMPObservedObject`, and `@KMPEnvironmentObject` mirror SwiftUI’s ownership language | | Field-level dependencies | On iOS 17+, only views that read an emitted projected field are invalidated | -| Shared collection | Multiple SwiftUI wrappers share one collector set per Kotlin model | +| Shared collection | Macro-configured wrappers share one static collector set per Kotlin model | | Native projection | `$viewModel.state` returns the current Swift value without exposing `.value` | | Safe bindings | Writable Kotlin exports produce `Binding`; read-only StateFlows remain read-only | | Deterministic lifetime | Collection, callback, Combine, and NativeFlow cancellation follow SwiftUI identity storage | @@ -357,7 +361,7 @@ All manifests expose the same products and public API. ## Validation -CI treats these as release-blocking: +Release validation covers: - Unit, lifecycle, cancellation, macro, and dependency-granularity tests - Strict concurrency with warnings as errors @@ -394,6 +398,10 @@ xcodebuild build \ - [DailyPulse integration guide](Examples/DailyPulse/iosApp/README.md) - [DocC catalog](Sources/KMPObservableBridge/Documentation/KMPObservableBridge.docc/KMPObservableBridge.md) - [Benchmark methodology](Benchmarks/RESULTS.md) +- [Evaluation, comparison, and migration guide](Docs/Evaluating-KMPObservableBridge.md) +- [Adoption playbook](ADOPTION.md) +- [Contributing](CONTRIBUTING.md) +- [Architecture discussion](https://github.com/sonmbol/KMPObservableBridge/discussions/16) - [Issue tracker](https://github.com/sonmbol/KMPObservableBridge/issues) ## License