Problem
The current macro requires every observable imported Kotlin property to be selected explicitly:
@KMPObservable(
ArticleViewModel.self,
fields: \.articleState, \.permissionsState
)
extension ArticleViewModel: @retroactive KMPStaticallyObservable {}
The ideal consumer API would allow:
@KMPObservable
extension ArticleViewModel: @retroactive KMPStaticallyObservable {}
and discover every supported exported observable property at compile time.
Hard constraints
Any proposed solution must:
- perform discovery at build/compile time, never through runtime reflection;
- avoid Objective-C selectors, swizzling,
Mirror, objc_msgSend, or method interception;
- preserve typed key paths and compile-time diagnostics;
- introduce no per-emission reflection, lookup, allocation, or actor hop;
- keep SKIE and NativeCoroutines integrations isolated from one another;
- avoid generated Swift source files that consumers must add to Xcode manually;
- preserve incremental-build correctness when the KMP framework changes;
- support large applications with hundreds of ViewModels;
- preserve the existing explicit
fields: API as a deterministic fallback.
Why the current macro cannot simply discover the fields
Swift macros operate on the syntax supplied to the expansion. They do not receive a general semantic reflection API for enumerating members of an imported Kotlin/Objective-C framework type. Build ordering alone does not change this limitation: the KMP/SKIE framework may exist before Swift compilation, but the macro expansion context still cannot query all imported members safely.
Approaches already considered
Swift macro-only discovery
Rejected as currently unavailable. An attached macro can inspect the annotated extension syntax, but not enumerate arbitrary members of the imported ViewModel declaration.
Freestanding or peer macro generating the conformance
Swift restricts the external names introduced by freestanding and peer macros. Earlier experiments found that these forms cannot legally synthesize an arbitrary extension/conformance for an unrelated imported type. Attaching the macro directly to the extension is the supported shape.
Objective-C runtime discovery
Rejected. SKIE-enhanced Swift declarations are not reliably represented by Objective-C property metadata, generic StateFlow types are erased or incomplete, and runtime lookup would undermine type safety, performance, and exporter isolation.
Runtime Swift reflection
Rejected. Mirror does not provide the required key paths or writable/current-value type relationships and would add runtime work and fragile implementation coupling.
Xcode/SwiftPM generator
Technically feasible, but previously removed because it introduces build-order and incremental-build complexity, DerivedData outputs, sandboxing concerns, plugin/toolchain coupling, and generated-file debugging. A generator also cannot inject information "into" a macro expansion invisibly: macros are compiler plugins, not a mutable source-generation destination. Any metadata must be made available through a compiler-visible source/module or another supported input mechanism.
Kotlin-generated metadata
Potentially viable, but adds Kotlin-side integration and may compromise the bridge's Swift-only dependency goal. This option should be evaluated only if metadata can remain exporter-neutral, deterministic, and optional.
Investigation areas
- New Swift macro/compiler APIs in future Swift toolchains that expose imported declaration members.
- SKIE-supported compile-time metadata surfaces intended for tooling.
- A SwiftPM build-tool plugin that produces private compiler-visible metadata without project-managed files.
- Kotlin/Native-generated static descriptors that do not require a bridge runtime dependency.
- A hybrid system where explicit fields remain source-compatible and generated descriptors are an opt-in optimization.
Performance and correctness risks
Automatic discovery could accidentally observe event streams, mutable implementation details, or expensive flows that were never intended to drive SwiftUI. Observing every export may increase collectors, Kotlin/ARC cross-boundary references, equality work, and global invalidations. Discovery must classify read-only state, writable state, and event streams correctly rather than treating every flow identically.
Any generated metadata must be invalidated when the KMP binary changes. Stale metadata is worse than explicit fields because it can compile against an outdated interface or silently miss state.
Acceptance criteria
@KMPObservable extension ViewModel: ... discovers supported state without runtime inspection.
- Clean and incremental builds regenerate/revalidate metadata correctly after adding, removing, or changing a Kotlin property.
- Unsupported or ambiguous properties produce compile-time diagnostics.
- Explicit
fields: and event-selection APIs remain available.
- No Objective-C runtime APIs or Swift reflection exist in the production path.
- No additional per-emission allocations, tasks, collectors, or View Graph invalidations compared with explicit key paths.
- Benchmarks and real SKIE/NativeCoroutines fixtures prove parity with the existing hot path.
Problem
The current macro requires every observable imported Kotlin property to be selected explicitly:
The ideal consumer API would allow:
and discover every supported exported observable property at compile time.
Hard constraints
Any proposed solution must:
Mirror,objc_msgSend, or method interception;fields:API as a deterministic fallback.Why the current macro cannot simply discover the fields
Swift macros operate on the syntax supplied to the expansion. They do not receive a general semantic reflection API for enumerating members of an imported Kotlin/Objective-C framework type. Build ordering alone does not change this limitation: the KMP/SKIE framework may exist before Swift compilation, but the macro expansion context still cannot query all imported members safely.
Approaches already considered
Swift macro-only discovery
Rejected as currently unavailable. An attached macro can inspect the annotated extension syntax, but not enumerate arbitrary members of the imported ViewModel declaration.
Freestanding or peer macro generating the conformance
Swift restricts the external names introduced by freestanding and peer macros. Earlier experiments found that these forms cannot legally synthesize an arbitrary extension/conformance for an unrelated imported type. Attaching the macro directly to the extension is the supported shape.
Objective-C runtime discovery
Rejected. SKIE-enhanced Swift declarations are not reliably represented by Objective-C property metadata, generic StateFlow types are erased or incomplete, and runtime lookup would undermine type safety, performance, and exporter isolation.
Runtime Swift reflection
Rejected.
Mirrordoes not provide the required key paths or writable/current-value type relationships and would add runtime work and fragile implementation coupling.Xcode/SwiftPM generator
Technically feasible, but previously removed because it introduces build-order and incremental-build complexity, DerivedData outputs, sandboxing concerns, plugin/toolchain coupling, and generated-file debugging. A generator also cannot inject information "into" a macro expansion invisibly: macros are compiler plugins, not a mutable source-generation destination. Any metadata must be made available through a compiler-visible source/module or another supported input mechanism.
Kotlin-generated metadata
Potentially viable, but adds Kotlin-side integration and may compromise the bridge's Swift-only dependency goal. This option should be evaluated only if metadata can remain exporter-neutral, deterministic, and optional.
Investigation areas
Performance and correctness risks
Automatic discovery could accidentally observe event streams, mutable implementation details, or expensive flows that were never intended to drive SwiftUI. Observing every export may increase collectors, Kotlin/ARC cross-boundary references, equality work, and global invalidations. Discovery must classify read-only state, writable state, and event streams correctly rather than treating every flow identically.
Any generated metadata must be invalidated when the KMP binary changes. Stale metadata is worse than explicit fields because it can compile against an outdated interface or silently miss state.
Acceptance criteria
@KMPObservable extension ViewModel: ...discovers supported state without runtime inspection.fields:and event-selection APIs remain available.