Swift (iOS) SDK
Native SwiftUI SDK for iOS 15+. Configure once, render billing recovery, cancel saves, and win back, and edit all copy and offers from the RedChurn dashboard without an App Store release.
Install with AI
Click a tool to copy a ready-to-paste integration prompt with your SDK key and code snippets.
https://github.com/redchurn/swift-sdkOverview
The Swift SDK connects your iOS app to RedChurn through a single API key. It registers your RevenueCat App User ID, syncs remote configuration, and renders native SwiftUI for three retention scenarios.
Everything visible to the subscriber (titles, messages, buttons, offers) is edited in the dashboard. The SDK polls for changes and updates automatically — you do not ship a new build to change copy.
- Cancel Save: intercept cancel taps with a native sheet
- Billing Recovery: in-app dunning when RevenueCat reports a billing issue
- Win back: reactivation prompt for churned subscribers
- Remote config: flows, copy, and offers controlled from the dashboard
Requirements
- iOS 15 or later (macOS 12+ also supported)
- Swift 5.9 / Xcode 15 or later
- SwiftUI (controllers are ObservableObject)
- RevenueCat configured in your app with App User IDs
- Push Notifications capability enabled if you use server push
1. Install (Swift Package Manager)
In Xcode: File → Add Package Dependencies, then paste the repository URL. Or add it to your Package.swift dependencies.
// Package.swift
dependencies: [
.package(url: "https://github.com/redchurn/swift-sdk", from: "0.4.0"),
],
// then add "RedChurn" to your target's dependencies2. Configure at launch
Call Redchurn.configure once in your @main App init(). Get your SDK key from the RedChurn dashboard (Settings, or during onboarding). Pass the RevenueCat App User ID so RedChurn can match subscribers to webhook events. Keep notificationsEnabled to schedule local reminders from your dashboard copy.
import SwiftUI
import Redchurn
import RevenueCat
@main
struct MyApp: App {
init() {
Redchurn.configure(
sdkKey: "rdchrn_live_xxxxxxxx",
rcAppUserId: Purchases.shared.appUserID,
notificationsEnabled: true
// debug: true // optional — prints SDK diagnostics
)
}
var body: some Scene {
WindowGroup { RootView() }
}
}3. Set the App User ID after login
If the RevenueCat App User ID is not available at launch, set it after authentication so prompts target the right subscriber:
// After your user logs in:
Redchurn.shared.setRCAppUserId(user.revenueCatAppUserId)4. Cancel Save
Create a RedchurnCancelFlowController and mount CancelSaveSheet on your settings screen. interceptCancel() shows the save sheet (survey then offers) and returns false when nothing should be shown, so you fall back to the store cancellation UI. Apply the accepted offer in onOfferAccepted via RevenueCat — throw on failure so RedChurn only counts a save after a successful store/RC operation.
import SwiftUI
import Redchurn
import RevenueCat
struct SubscriptionSettings: View {
@StateObject private var cancelFlow = RedchurnCancelFlowController(
rcAppUserId: Purchases.shared.appUserID,
onOfferAccepted: { offer in
try await applyRetentionOffer(offer) // throw on failure — save only on success
}
)
var body: some View {
VStack {
Button("Cancel subscription") {
Task {
let intercepted = await cancelFlow.interceptCancel()
if !intercepted {
try? await AppStore.showManageSubscriptions(in: scene)
}
}
}
CancelSaveSheet(controller: cancelFlow)
}
}
}5. Billing Recovery
When RevenueCat reports a billing issue, RedChurn shows an in-app dunning banner on the next open and (with notificationsEnabled) schedules local reminders. Mount BillingRecoveryBanner near the top of your home screen and re-check on notification taps.
import SwiftUI
import Redchurn
import RevenueCat
struct HomeView: View {
@StateObject private var billing = RedchurnBillingRecoveryController(
rcAppUserId: Purchases.shared.appUserID
)
var body: some View {
VStack {
BillingRecoveryBanner(controller: billing)
YourHomeContent()
}
.onReceive(NotificationCenter.default.publisher(for: .redchurnDidOpenFromNotification)) { note in
guard let flow = note.object as? RedchurnFlow, flow == .billingRecovery else { return }
Task { await billing.check() }
}
}
}6. Win back
For churned subscribers (RevenueCat EXPIRATION), WinBackPrompt shows your reactivation offer on next open. Route the CTA to your paywall.
import SwiftUI
import Redchurn
import RevenueCat
struct HomeView: View {
@StateObject private var winBack = RedchurnWinBackController(
rcAppUserId: Purchases.shared.appUserID
)
var body: some View {
YourHomeContent()
.background(
WinBackPrompt(controller: winBack, onCtaPress: { openPaywall() })
)
.onReceive(NotificationCenter.default.publisher(for: .redchurnDidOpenFromNotification)) { note in
guard let flow = note.object as? RedchurnFlow, flow == .winBack else { return }
Task { await winBack.check() }
}
}
}7. Route notification taps
So a tap on a billing recovery / win back notification opens the right flow, forward the payload to RedchurnNotificationRouting from your UNUserNotificationCenterDelegate. It posts redchurnDidOpenFromNotification (handled by the controllers above) and ignores unrelated notifications.
import UserNotifications
import Redchurn
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
if RedchurnNotificationRouting.handleNotificationResponse(
userInfo: response.notification.request.content.userInfo
) != nil {
completionHandler()
return
}
// your other notification handling…
completionHandler()
}8. Server push (optional)
Local notifications only fire while the app has run. To reach subscribers who never reopen the app, RedChurn also sends server push via APNs, triggered by the RevenueCat webhook. On iOS you must forward the native APNs device token — it is not auto-registered.
Request authorization, register for remote notifications, then forward the token in your AppDelegate. Add your APNs .p8 key (Key ID, Team ID, Bundle ID) once in Dashboard → Integrations → Push notifications.
- Credentials are per app (BYOK): APNs .p8 key in Dashboard → Integrations
- Sandbox vs Production: Debug builds from Xcode use sandbox; TestFlight and App Store use production
- Test end-to-end from Dashboard → Events → Test push notifications
import UIKit
import UserNotifications
import Redchurn
// 1. Ask permission and register with APNs (e.g. after login)
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
guard granted else { return }
DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }
}
// 2. Forward the APNs token to RedChurn
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Task { await Redchurn.shared.registerPushToken(deviceToken) }
}Deeplinks (email → app)
Open the right flow from an email CTA (or any link) and track the open. Your app owns its URL scheme / associated domains; the SDK interprets the URL.
Redchurn.shared.handleDeeplink(url) parses the URL, records a LINK_OPENED event with the flow, source and campaign for attribution, and posts redchurnDidOpenFromNotification with the flow — so the observer you already wired for notification taps routes it too.
// SwiftUI
.onOpenURL { url in Task { await Redchurn.shared.handleDeeplink(url) } }
// e.g. myapp://redchurn?flow=billing_recovery&campaign=oct&source=emailRemote configuration
After the SDK is installed, all subscriber-facing content is managed in the dashboard. Toggle flows on or off, edit copy, change offers, and update styling. Changes propagate within 60 seconds.
The SDK polls a lightweight /version endpoint and only re-downloads configuration when something changed, plus a refresh when the app returns to foreground. Observe Redchurn.shared.ready / remoteConfig in SwiftUI if you need to react to it.
- In-app flows and copy: Dashboard → Scenarios → In-app
- Cancel offers: Dashboard → Cancel saves
- Email sequences: Dashboard → Scenarios → Email
- Branding and fallback CTA: Dashboard → Settings
How the SDK talks to RedChurn
All networking goes through an internal actor that talks to https://app.redchurn.io, authenticated with your SDK key as a Bearer token. Requests have a 12 second timeout and only /outcomes is retried, because it is the call that credits saved MRR.
rcAppUserId is the key that joins the SDK to your RevenueCat webhook events server-side, so a billing issue detected by webhook surfaces the right in-app prompt for the same subscriber.
Authorization: Bearer rdchrn_live_xxxxxxxx
Accept: application/json
X-RedChurn-Sdk-Version: 0.4.0
Accept-Language: fr # when a locale is setPOST /api/sdk/v1/register heartbeat + APNs token registration
GET /api/sdk/v1/config full remote config (flows, copy, offers)
GET /api/sdk/v1/version lightweight change check (polled)
GET /api/sdk/v1/prompts contextual billing-recovery / win-back prompts
POST /api/sdk/v1/events APP_OPEN, CANCEL_SESSION, PROMPT_SHOWN/DISMISSED, LINK_OPENED
POST /api/sdk/v1/outcomes saved / declined / dismissed (retried)Fail-safe behavior
The SDK never throws into your app. With an invalid SDK key it runs in no-op mode and your views render normally. interceptCancel() is fail-open so a subscriber can always reach the store.
Use the onError callback to log non-fatal issues to your monitoring tool.
Redchurn.configure(
sdkKey: "rdchrn_live_xxxxxxxx",
rcAppUserId: Purchases.shared.appUserID,
onError: { error in
// Sentry.capture(message: "[RedChurn] \(error)")
}
)Production checklist
- RevenueCat webhook connected and delivering events
- SDK key from production environment (rdchrn_live_...)
- App User ID set for every authenticated subscriber
- Cancel button wired through interceptCancel(), not a direct store link
- BillingRecoveryBanner and WinBackPrompt mounted on your home screen
- Notification taps routed via RedchurnNotificationRouting
- APNs key added in Dashboard → Integrations and token forwarded (if using server push)
- In-app copy and offers reviewed in dashboard before launch
Install from SwiftPM or download
SwiftPM is the recommended path. You can also download the SDK archive from the button at the top of this page and drag the folder into your project, or mirror it in a private Git repo.
.package(url: "https://github.com/redchurn/swift-sdk", from: "0.4.0")