Context
A very common KMP architecture is shared Kotlin for logic, fully native SwiftUI for iOS UI — no Compose Multiplatform on iOS. Before 3.0, this worked naturally: the SPM PurchasesHybridCommon(UI) copy was the single SDK instance, and Swift code could use PaywallView/CustomerCenterView directly.
With 3.x bundling purchases-ios inside the Kotlin framework, that door closed: import RevenueCat no longer compiles, and re-adding the SPM package creates a second SDK copy that crashes at startup (#882, where it was confirmed the Kotlin API is the only supported way). The officially supported UI module (purchases-kmp-ui) is Compose-only — so SwiftUI hosts currently have no supported way to show paywalls or the Customer Center.
We completed this migration (purchases-kmp 3.0.4, static framework, Xcode 26.6). It works, but here is everything a SwiftUI-host team has to do today:
What a native-UI host must do today
- Discover the constraint — the 3.0.0 migration guide says to remove the SPM packages but never mentions that native
import RevenueCat/import RevenueCatUI code stops working or what to do instead; the answer lives in an issue comment.
- Depend on internal-looking binding artifacts —
com.revenuecat.purchases:purchases-kmp-kn-ui and the com.revenuecat.purchases.kn.ui package, which aren't documented as public API.
- Hand-write a Kotlin iosMain bridge, dealing with several footguns:
RCPaywallViewController's identifier-based inits aren't exposed (non-@objc/@_spi), so you construct with offering = null and later call the deprecated updateWithOfferingIdentifier:/updateWithOffering: after resolving via awaitOfferings().
- Implementing
RCPaywallViewControllerDelegateProtocol requires casts between duplicate cinterop types for the same Obj-C classes (kn-core's RCPackage vs kn-ui's), with @Suppress("CAST_NEVER_SUCCEEDS").
- The VC delegates are weak; the bridge must retain them for the controller's lifetime or callbacks silently stop.
- Expose the
UIViewControllers through the framework and host them in SwiftUI via UIViewControllerRepresentable.
- Manually embed RevenueCatUI's resources — the bundled RevenueCatUI is compiled with
COMPOSE_RESOURCES and preconditionFailures at first render unless compose-resources/composeResources/com.revenuecat.purchases.kn.ui.resources/files/… exists in the app bundle. The files ship in the kotlin_resources Maven artifact that only the Compose Gradle plugin consumes, so a non-Compose host needs a custom Gradle unzip task plus an Xcode script-phase rsync.
- Accept reduced Customer Center fidelity — the
@objc RCCustomerCenterViewController hardcodes default CustomerCenterNavigationOptions, so options the SwiftUI CustomerCenterView exposes (usesExistingNavigation, usesNavigationStack, shouldShowCloseButton) aren't reachable; pushed presentations get a nested navigation bar.
Proposal
An official, documented native-UI bridge — e.g. a purchases-kmp-ui-uikit module (or a public surface in kn-ui) that exports from the Kotlin framework:
// Exported to Swift via the shared framework
class PaywallViewController(
offeringIdentifier: String? = null,
placementIdentifier: String? = null,
listener: PaywallListener? = null, // strongly retained internally
) { val viewController: UIViewController }
class CustomerCenterViewController(
navigationOptions: CustomerCenterNavigationOptions = CustomerCenterNavigationOptions.default,
listener: CustomerCenterListener? = null,
) { val viewController: UIViewController }
With:
- offering/placement resolution built in (no deprecated update calls in app code),
- internal delegate retention (no weak-delegate footgun),
- callbacks carrying KMP model types (
Package, CustomerInfo) or primitives, reusing the existing PaywallListener,
- resource embedding handled by
embedAndSignAppleFrameworkForXcode (it already runs inside Xcode with $TARGET_BUILD_DIR available) — or, failing that, a graceful English fallback instead of preconditionFailure,
- Customer Center navigation options exposed, and
- a docs page with the ~10-line
UIViewControllerRepresentable Swift snippet.
That would reduce the entire list above to "add one dependency, call one factory, host the view controller".
Alternatives we considered
Happy to share our working bridge implementation (paywall + customer center + resource-embedding Gradle/Xcode wiring) as a starting point if that's useful.
Context
A very common KMP architecture is shared Kotlin for logic, fully native SwiftUI for iOS UI — no Compose Multiplatform on iOS. Before 3.0, this worked naturally: the SPM
PurchasesHybridCommon(UI)copy was the single SDK instance, and Swift code could usePaywallView/CustomerCenterViewdirectly.With 3.x bundling
purchases-iosinside the Kotlin framework, that door closed:import RevenueCatno longer compiles, and re-adding the SPM package creates a second SDK copy that crashes at startup (#882, where it was confirmed the Kotlin API is the only supported way). The officially supported UI module (purchases-kmp-ui) is Compose-only — so SwiftUI hosts currently have no supported way to show paywalls or the Customer Center.We completed this migration (purchases-kmp 3.0.4, static framework, Xcode 26.6). It works, but here is everything a SwiftUI-host team has to do today:
What a native-UI host must do today
import RevenueCat/import RevenueCatUIcode stops working or what to do instead; the answer lives in an issue comment.com.revenuecat.purchases:purchases-kmp-kn-uiand thecom.revenuecat.purchases.kn.uipackage, which aren't documented as public API.RCPaywallViewController's identifier-based inits aren't exposed (non-@objc/@_spi), so you construct withoffering = nulland later call the deprecatedupdateWithOfferingIdentifier:/updateWithOffering:after resolving viaawaitOfferings().RCPaywallViewControllerDelegateProtocolrequires casts between duplicate cinterop types for the same Obj-C classes (kn-core'sRCPackagevs kn-ui's), with@Suppress("CAST_NEVER_SUCCEEDS").UIViewControllers through the framework and host them in SwiftUI viaUIViewControllerRepresentable.COMPOSE_RESOURCESandpreconditionFailures at first render unlesscompose-resources/composeResources/com.revenuecat.purchases.kn.ui.resources/files/…exists in the app bundle. The files ship in thekotlin_resourcesMaven artifact that only the Compose Gradle plugin consumes, so a non-Compose host needs a custom Gradle unzip task plus an Xcode script-phasersync.@objc RCCustomerCenterViewControllerhardcodes defaultCustomerCenterNavigationOptions, so options the SwiftUICustomerCenterViewexposes (usesExistingNavigation,usesNavigationStack,shouldShowCloseButton) aren't reachable; pushed presentations get a nested navigation bar.Proposal
An official, documented native-UI bridge — e.g. a
purchases-kmp-ui-uikitmodule (or a public surface in kn-ui) that exports from the Kotlin framework:With:
Package,CustomerInfo) or primitives, reusing the existingPaywallListener,embedAndSignAppleFrameworkForXcode(it already runs inside Xcode with$TARGET_BUILD_DIRavailable) — or, failing that, a graceful English fallback instead ofpreconditionFailure,UIViewControllerRepresentableSwift snippet.That would reduce the entire list above to "add one dependency, call one factory, host the view controller".
Alternatives we considered
Happy to share our working bridge implementation (paywall + customer center + resource-embedding Gradle/Xcode wiring) as a starting point if that's useful.