Skip to content

Add default automatic SKIE StateFlow discovery - #3

Merged
sonmbol merged 9 commits into
mainfrom
feature/automatic-skie-state-discovery
Jul 24, 2026
Merged

Add default automatic SKIE StateFlow discovery#3
sonmbol merged 9 commits into
mainfrom
feature/automatic-skie-state-discovery

Conversation

@sonmbol

@sonmbol sonmbol commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

Adds automatic SKIE StateFlow observation as the default no-state behavior:

@KMPStateObject
private var profile = ProfileViewModel()

Externally owned models use the same default:

@KMPObservedObject private var profile: ProfileViewModel

init(profile: ProfileViewModel) {
    _profile = KMPObservedObject(profile)
}

The strategy can still be written explicitly as .automaticSKIE, while .none disables automatic subscriptions:

@KMPStateObject(observation: .none)
private var profile = ProfileViewModel()

No generated Swift file, Xcode project mutation, build script, macro, Kotlin bridge dependency, or $ registration is required. Existing state:, states:, adapter, generated-conformance, and KMP-NativeCoroutines APIs remain available and unchanged. Explicit state/adapters bypass runtime discovery.

How discovery works

Kotlin/Native does not emit usable Objective-C property metadata for these properties, so Mirror and class_copyPropertyList cannot enumerate them. Kotlin property getters are still exported as Objective-C methods.

The bridge installs a guarded, one-time interceptor for eligible getters on the concrete Kotlin class. It never calls a getter speculatively. When application code naturally reads a getter, the bridge:

  1. calls the original implementation;
  2. checks whether the returned object conforms to that framework's exported Kotlinx_coroutines_coreStateFlow protocol;
  3. finds the matching SKIE cancellable iterator in the same framework image;
  4. establishes one deduplicated observation for the getter and flow identity;
  5. returns the original value unchanged.

A replacement flow identity cancels the previous iterator. Multiple Swift stores can listen to the same Kotlin model without duplicate getter discovery, and the last listener teardown cancels active collections. Store generations continue suppressing stale emissions during rebinding.

Compatibility considerations

Automatic SKIE discovery is the convenience default, but explicit key paths remain the maximum-stability option:

  • It uses process-wide Objective-C method interception.
  • It dynamically calls SKIE's generated SkieColdFlowIterator ABI, which is not documented as a stable third-party runtime API.
  • Kotlin/Native or SKIE may change generated names or method signatures.
  • Another runtime library could intercept the same getter.
  • Only naturally accessed flows are observed, so conditional UI can establish observations at different times.
  • Runtime ABI failures are harder to diagnose than statically typed AsyncSequence key paths.

The implementation checks the framework image, protocol, iterator class, and required selectors before activation. Unsupported frameworks safely create no automatic observation. Applications can use explicit state key paths for deterministic observation or .none to disable automatic behavior entirely.

Safety and lifecycle behavior

  • Interceptors are installed once per concrete class and selector under a lock.
  • Unknown getters and methods are never invoked for discovery.
  • Non-StateFlow return values are ignored.
  • Repeated getter reads are deduplicated.
  • Flow replacement cancels the stale iterator.
  • Listener removal and iterator cancellation are idempotent.
  • Model/store retain cycles are avoided through weak callbacks.
  • UI invalidations still pass through the store's main-actor generation and update-policy handling.
  • SKIE/Kotlin cancellation exceptions are treated as expected lifecycle termination rather than observation failures.
  • Runtime discovery is restricted to models and SKIE iterators from the same loaded framework image.
  • .none preserves ownership, disposal, model access, and rebinding while installing no automatic subscriptions.

API and documentation

  • Adds KMPAutomaticObservation.automaticSKIE as the default automatic strategy.
  • Adds KMPAutomaticObservation.none as the explicit disable strategy.
  • Supports owning, injector, wrapped-value, and externally observed initialization.
  • DailyPulse demonstrates annotation-free default usage with real generated SKIE flows.
  • README and DocC explain operation, limitations, failure behavior, .none, and when explicit key paths should be preferred.
  • No project.pbxproj changes and no XcodeProj dependency.

Verification

  • swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors — 30 tests passed.
  • DailyPulse built against its generated Kotlin/SKIE framework using the default syntax.
  • The app installed and launched successfully in the iOS simulator.
  • Runtime proof observed the initial StateFlow replay and a subsequent Kotlin emission, then cancelled the iterator.
  • git diff --check passed.

Relates to #2

@sonmbol sonmbol changed the title Add optional automatic SKIE StateFlow discovery Add opt-in automatic SKIE StateFlow discovery Jul 24, 2026
@sonmbol

sonmbol commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Implemented in 8989a52. Automatic SKIE discovery is now explicitly opt-in via .automaticSKIE; the PR description and README document the Objective-C interception, generated iterator ABI, conditional-access, collision, debugging, fallback, lifecycle, and compatibility considerations. Verification is green: 28 strict-concurrency tests passed, and DailyPulse built, installed, and launched against the real generated SKIE framework.

@sonmbol

sonmbol commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Follow-up b067cfe adds KMPAutomaticObservation.none. Both KMPStateObject and KMPObservedObject now support an explicit no-observation mode that preserves ownership/rebinding but installs no automatic subscriptions. README includes owned and externally observed examples plus the no-invalidation warning. Strict-concurrency verification now passes 29 tests.

@sonmbol sonmbol changed the title Add opt-in automatic SKIE StateFlow discovery Add default automatic SKIE StateFlow discovery Jul 24, 2026
@sonmbol

sonmbol commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Follow-up 4a9a82b makes .automaticSKIE the default. @KMPStateObject, @KMPObservedObject with backing initialization, wrapped-value observation, and injector initialization now use automatic SKIE discovery without spelling the strategy. .none remains the explicit disable path, and state/adapters remain deterministic overrides. README, DocC, DailyPulse, and the PR description were updated. Verification: 30 strict-concurrency tests passed; DailyPulse built, installed, and launched.

@sonmbol

sonmbol commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Architecture cleanup in f13a9be: removed the obsolete kmp-observable-bridge-generator executable product, KMPObservableBridgeGenerator target, and its 343-line source. The package now exposes exactly one product and two targets (library + tests). Runtime automatic SKIE discovery is the single authoritative automatic mechanism; explicit states and .none remain controlled alternatives. README and changelog were corrected. Verification: manifest inspection, 30 strict-concurrency tests, API check, and DailyPulse build/install/launch all passed.

@sonmbol

sonmbol commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Final routing cleanup in c8b94e5:

  • Removed KMPObservedObjectHolder; KMPObservedObject now directly owns one KMPViewModelStore through @StateObject.
  • Replaced the states + automaticStateFlowDiscovery combination with one mutually exclusive internal source: .none, .automaticSKIE, or .explicit. Invalid mixed modes are no longer representable.
  • Removed reliance on Swift overload ranking. Before the store is created, the bridge checks .none, then an explicit KMPAutomaticallyObservable/KMPNativeObservable contract, and only then falls back to SKIE.
  • A NativeCoroutines model therefore installs its NativeFlow callback without any SKIE protocol/iterator lookup or getter interception. Explicit state/adapters also bypass SKIE.
  • .none overrides even an available NativeFlow contract.
  • Simplified and humanized README examples and added a clear routing table.

Verification: 30 strict-concurrency tests passed, including direct NativeFlow routing and .none precedence; API check passed; DailyPulse built, installed, and launched against the generated framework.

@sonmbol
sonmbol merged commit ca838ea into main Jul 24, 2026
1 check passed
@sonmbol
sonmbol deleted the feature/automatic-skie-state-discovery branch July 24, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant