Version 4.x.x of the iOS SDK just released on 1/27/26. To migrate from v3, view the migration guide. You can also view the v3 guide here.
Helium requires a minimum deployment target of iOS 15 and Xcode 14+. (Latest Xcode is recommended.)
We recommend using Swift Package Manager (SPM), but if your project primarily uses Cocoapods it might make sense to install the Helium Cocoapod instead.
Swift Package Manager (SPM)
Cocoapod
In Xcode, navigate to your project’s Package Dependencies:
Click the + button and search for the Helium package URL:
For Dependency Rule we recommend the default Up to Next Major Version to make sure you get non-breaking bug fixes. View the list of releases here.
Click Add Package.
In the dialog that appears, make sure to add the Helium product to your app’s main target:
Select Add Package in the dialog and Helium should now be ready for import.
(Optional) If you are using RevenueCat to manage purchases, you’ll need to add the HeliumRevenueCat package separately. This is a separate package from the core Helium SDK:
Add the HeliumRevenueCat product to your app’s main target.
This enables the RevenueCatDelegate referenced in the Purchase Handling section of this guide.
The HeliumRevenueCat package includes purchases-ios-spm as a dependency, notpurchases-ios and you may encounter build issues if you are using purchases-ios with SPM. (We recommend just switching to purchases-ios-spm).
Choose the appropriate location based on your app’s architecture:
SwiftUI
SceneDelegate
AppDelegate
Copy
Ask AI
@mainstruct MyApp: App { init() { // Add this: configureHelium() } var body: some Scene { WindowGroup { ContentView() } } // And this: private func configureHelium() { // Identify user and adjust Helium.config if needed (see next sections). // Then call initialize: Helium.shared.initialize(apiKey: "helium-api-key") }}
Copy
Ask AI
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Add this: configureHelium() } // And this: private func configureHelium() { // Identify user and adjust Helium.config if needed (see next sections). // Then call initialize: Helium.shared.initialize(apiKey: "helium-api-key") }}
Copy
Ask AI
@UIApplicationMainclass AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Add this: configureHelium() return true } // And this: private func configureHelium() { // Identify user and adjust Helium.config if needed (see next sections). // Then call initialize: Helium.shared.initialize(apiKey: "helium-api-key") }}
And add necessary imports:
Copy
Ask AI
import Helium
Helium’s initialization is ran on a background thread, so you don’t have to worry about it affecting your app’s launch time.
Identifying users is optional but can help with targeting and when forwarding events to external analytics platforms.
If you are not sure, you probably do not need to identify your users.
Identify users as early as you can to maximize consistency in metrics and targeting. Ideally right before you call Helium.shared.initialize!
if let appAccountTokenUUID = UUID(uuidString: "app-account-token-uuid") { Helium.identify.appAccountToken = appAccountTokenUUID}
Set custom user traits for targeting and analytics visibility
Copy
Ask AI
Helium.identify.setUserTraits(HeliumUserTraits(["hasOnboarded": true]))// or Helium.identify.addUserTraits() if you don't want to clear existing traits
You must have a trigger and workflow configured in the dashboard in order to show a paywall.
Call presentPaywall when you want to show a full-screen paywall. For example:
Copy
Ask AI
Helium.shared.presentPaywall( trigger: "premium") { paywallNotShownReason in switch paywallNotShownReason { case .targetingHoldout: break case .alreadyEntitled: // e.g. ensure premium access // In order for this case to be hit, `config.dontShowIfAlreadyEntitled` must be true break default: // handle the rare case where a paywall // fails to show (see Fallbacks section on this page) break }}
(Optional) Configuration for this paywall presentation
Copy
Ask AI
struct PaywallPresentationConfig { // View controller to present from. Defaults to current top view controller var presentFromViewController: UIViewController? = nil // Custom traits to send to the paywall var customPaywallTraits: [String: Any]? = nil // Don't show paywall if user is entitled to a product in paywall var dontShowIfAlreadyEntitled: Bool = false // How long to allow loading state before switching to fallback logic. // Use zero or negative value to disable loading state. var loadingBudget: TimeInterval = DEFAULT_LOADING_BUDGET}
Handle any scenario where the paywall does not show. If user is already entitled and config.dontShowIfAlreadyEntitled is true, onPaywallNotShown(.alreadyEntitled) will be called only if onEntitled is not provided.
You should now be able to see Helium paywalls in your app! Well done! 🎉
Looking for alternative presentation methods? Check out the guide on Ways to Show a Paywall.
When displaying a paywall you can pass in event handlers to listen for relevant Helium Events. You can chain a subset of handlers with builder syntax:
Copy
Ask AI
Helium.shared.presentPaywall( trigger: "post_onboarding", eventHandlers: PaywallEventHandlers() .onOpen { event in print("open via trigger \(event.triggerName)") } .onClose { event in print("close for trigger \(event.triggerName)") } .onDismissed { event in print("dismiss for trigger \(event.triggerName)") } .onPurchaseSucceeded { event in print("purchase succeeded for trigger \(event.triggerName)") } .onCustomPaywallAction { event in print("Custom action: \(event.actionName) with params: \(event.params)") } .onAnyEvent { event in // A handler for all paywall-related events. // Note that if you have other handlers (i.e. onOpen) set up, // both that handler AND this one will fire during paywall open. }) { paywallNotShownReason in // handle paywall not shown}
By default, Helium will handle purchases for you! This section is for those who want to delegate purchases to RevenueCat or implement custom purchase logic.
Use one of our pre-built HeliumPurchaseDelegate implementations or create a custom delegate. Pass the delegate in to your Helium.shared.initialize call.
StoreKitDelegate
RevenueCatDelegate
Custom Delegate
The StoreKitDelegate (default delegate) handles purchases using native StoreKit 2:
Want to add some custom behavior but still use the built-in purchase logic? Just subclass StoreKitDelegate or RevenueCatDelegate! (Be sure to make a super call for any overridden methods.)
Make sure you included HeliumRevenueCat (for SPM) or Helium/RevenueCat (for Cocoapod) as noted in the Installation section.
Use RevenueCatDelegate to handle purchases through RevenueCat:
Copy
Ask AI
import HeliumRevenueCat // unless using Cocoapod then can just import Heliumlet heliumPurchaseDelegate = RevenueCatDelegate( // Optional - pass in to have Helium to handle RevenueCat initialization. revenueCatApiKey: "<revenue-cat-api-id>")Helium.config.purchaseDelegate = heliumPurchaseDelegate
If you do not supply revenueCatApiKey, make sure to initialize RevenueCat before creating the RevenueCatDelegate!
You can also create a custom delegate and implement your own purchase logic. You can look at our StoreKitDelegate and RevenueCatDelegate in the SDK for examples (also linked below).The HeliumPaywallDelegate is defined as follows:
Copy
Ask AI
public protocol HeliumPurchaseDelegate: AnyObject { // Execute the purchase of a product given the product ID. func makePurchase(productId: String) async -> HeliumPaywallTransactionStatus // (Optional) - Restore any existing subscriptions. // Return a boolean indicating whether the restore was successful. func restorePurchases() async -> Bool // (Optional) - Called for all Helium events (e.g. PaywallOpenEvent) func onPaywallEvent(_ event: HeliumEvent)}
HeliumPaywallTransactionStatus is an enum that defines the possible states of a paywall transaction:
Copy
Ask AI
public enum HeliumPaywallTransactionStatus { case purchased case cancelled case failed(Error) case restored case pending}
Visit Helium Events for details on the different Helium paywall events.
When executing the purchase via StoreKit 2 (recommended over StoreKit 1), please use Product.heliumPurchase() instead of Product.purchase(). For example:let result = try await product.heliumPurchase()This will automatically set attribution information for revenue tracking.
StoreKitDelegate example here.RevenueCatDelegate example here.
/// Implement this where you want to handle eventspublic protocol HeliumEventListener : AnyObject { func onHeliumEvent(event: HeliumEvent)}/// Add a listener for all Helium events.public func addHeliumEventListener(_ listener: HeliumEventListener)/// Remove a specific Helium event listener.public func removeHeliumEventListener(_ listener: HeliumEventListener)
Listeners are held weakly to prevent memory leaks. If you don’t maintain a strong reference to your listener, it will be deallocated immediately and no events will fire.
Copy
Ask AI
// ❌ Wrong - listener is deallocated immediately, no events will fireHelium.shared.addHeliumEventListener(MyListener())// ✅ Works - singleton keeps a strong referenceclass MyHeliumEventListener: HeliumEventListener { static let shared = MyHeliumEventListener() func onHeliumEvent(event: any HeliumEvent) { print("Helium event: \(event.toDictionary())") }}// And make sure to register it:Helium.shared.addHeliumEventListener(MyHeliumEventListener.shared)
If you use an external payment processor like Stripe, Helium’s entitlement helpers may not be reliable. We recommend implementing your own entitlement checking in that case. If you use Stripe with RevenueCat, we recommend using RevenueCat’s entitlement APIs instead.
The Helium SDK provides several ways to check user entitlements and subscription status.
Entitlement Helper Methods
hasAny() Checks if the user has purchased any subscription or non-consumable product.hasAnyActiveSubscription() Checks if the user has any active subscription.hasEntitlementForPaywall(trigger: String, considerAssociatedSubscriptions: Bool = false) Checks if the user has entitlements for any product in a specific paywall. Returns nil if paywall configuration hasn’t been downloaded yet.hasActiveEntitlementFor(productId: String) Checks if the user has entitlement to a specific product.hasActiveSubscriptionFor(productId: String) Checks if the user has an active subscription for a specific product.hasActiveSubscriptionFor(subscriptionGroupID: String) Checks if the user has an active subscription in a specific subscription group.purchasedProductIds() Retrieves a list of all product IDs the user currently has access to.activeSubscriptions() Returns detailed information about all active auto-renewing subscriptions.subscriptionStatusFor(productId: String) Gets detailed subscription status for a specific product, including state information like subscribed, expired, or in grace period.subscriptionStatusFor(subscriptionGroupID: String) Gets detailed subscription status for a specific subscription group.
It is highly recommended that you set up a fallbacks in the uncommon case where a paywall fails to display. Please follow the linked guide to do so.Note that if you attempt to display a paywall while it is still being downloaded, a loading state will show.By default, Helium will show this loading state as needed (a shimmer view for up to 7 seconds). You can configure this loading state during presentation or set global values.
If the budget expires before the paywall is ready, a fallback paywall will show if available otherwise the loading state will hide and a PaywallOpenFailed event will be dispatched.
See the Fallbacks Guide for more details on downloading and configuring fallbacks.
In most cases there is no need to check download status. Helium will display a loading indication if a paywall is presented before download has completed.
You can check the status of the paywall configuration download using the Helium.shared.getDownloadStatus() method. This method returns a value of type HeliumFetchedConfigStatus, which is defined as follows:
Copy
Ask AI
public enum HeliumFetchedConfigStatus: String, Codable, Equatable { case notDownloadedYet case inProgress case downloadSuccess case downloadFailure}
You can also simply check if paywalls have been successfully downloaded with Helium.shared.paywallsLoaded().
Get Paywall Info By Trigger
Retrieve basic information about the paywall for a specific trigger with Helium.shared.getPaywallInfo(trigger: String) which returns:
Copy
Ask AI
public struct PaywallInfo { public let paywallTemplateName: String // shouldShow only false if the paywall should not be shown due to targeting or workflow configuration (Helium handles this for you in presentUpsell) public let shouldShow: Bool}
This method can be used if you want to be certain that a paywall is ready for display before displaying.
Hiding Paywalls Programmatically
You can programmatically hide paywalls using:
Copy
Ask AI
// Hide the current paywallHelium.shared.hidePaywall()// Hide all currently displayed paywallsHelium.shared.hideAllPaywalls()
Reset Helium
Reset Helium entirely so you can call initialize again. Only for advanced use cases.