Clix iOS SDK is a powerful tool for managing push notifications and user events in your iOS application. It provides a simple and intuitive interface for user engagement and analytics.
dependencies: [
.package(url: "https://github.com/clix-so/clix-ios-sdk.git", from: "1.6.0")
]pod 'Clix'- iOS 15.0 or later
- Swift 5.5 or later
If you need to use Firebase 12 or later, you must use the latest version of Clix iOS SDK. Previous versions may not be compatible with Firebase 12+.
Initialize the SDK with a ClixConfig object. The config is required and contains your project settings.
import Clix
let config = ClixConfig(
projectId: "YOUR_PROJECT_ID",
apiKey: "YOUR_API_KEY",
endpoint: "https://api.clix.so", // Optional: default is https://api.clix.so
logLevel: .debug, // Optional: set log level
extraHeaders: [:] // Optional: extra headers for API requests
)
await Clix.initialize(config: config)All SDK methods provide both async/await and synchronous versions. The async versions are recommended for better control over operation timing.
// Async version (recommended)
try await Clix.setUserId("user123")
// Synchronous version
Clix.setUserId("user123")// Set user ID
try await Clix.setUserId("user123")
// Set user properties
try await Clix.setUserProperty("name", value: "John Doe")
try await Clix.setUserProperties([
"age": 25,
"premium": true
])
// Remove user properties
try await Clix.removeUserProperty("name")
try await Clix.removeUserProperties(["age", "premium"])
// Remove user ID
try await Clix.removeUserId()// Track an event with properties
try await Clix.trackEvent(
"signup_completed",
properties: [
"method": "email",
"discount_applied": true,
"trial_days": 14,
"completed_at": Date()
]
)// Get device ID
let deviceId = await Clix.getDeviceId()
// Get push token
let pushToken = await Clix.getPushToken()Clix.setLogLevel(.debug)
// Available log levels:
// - .none: No logs
// - .error: Error logs only
// - .warn: Warning logs
// - .info: Info logs
// - .debug: Debug logsClix SDK supports two integration paths:
ClixAppDelegatesubclassing (quick start, minimal code)Clix.Notificationstatic helper (manual wiring, fine-grained control)
This approach automates push notification registration, permission requests, device token management, and event tracking.
-
Enable Push Notifications in Xcode
- In your project, go to Signing & Capabilities.
- Add Push Notifications and Background Modes (check Remote notifications).
-
Inherit from ClixAppDelegate in your AppDelegate
If you want the quickest working setup with sensible defaults, subclass ClixAppDelegate and initialize the SDK. Firebase is only needed if you use FCM.
import UIKit
import Clix
import Firebase
@main
class AppDelegate: ClixAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Configure Firebase first (before calling super)
FirebaseApp.configure()
let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
// Required: initialize Clix with your credentials
Task {
let config = ClixConfig(
projectId: "YOUR_PROJECT_ID",
apiKey: "YOUR_API_KEY"
)
await Clix.initialize(config: config)
}
return result
}
}import UIKit
import Firebase
import Clix
@main
class AppDelegate: ClixAppDelegate {
// Optional: delay the system permission prompt until your onboarding is ready.
// SDK default is `false`. Override to `true` to auto-request permissions on launch.
override var autoRequestPermission: Bool { true }
// Optional: prevent automatic deep-link opening on push tap; route manually instead.
// Remove or change to `true` to use SDK default behavior.
override var autoHandleLandingURL: Bool { false }
// Optional: force foreground presentation options instead of SDK logic.
override func willPresentOptions(for notification: UNNotification) -> UNNotificationPresentationOptions? {
if #available(iOS 14.0, *) { return [.list, .banner, .sound, .badge] }
else { return [.alert, .sound, .badge] }
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Configure Firebase first (before calling super)
FirebaseApp.configure()
let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
// Initialize Clix SDK
Task {
let config = ClixConfig(
projectId: "YOUR_PROJECT_ID",
apiKey: "YOUR_API_KEY",
logLevel: .debug
)
await Clix.initialize(config: config)
}
// Optional: since autoHandleLandingURL is set to false above, handle routing yourself if needed.
Clix.Notification.onNotificationOpened { userInfo in
if let clix = userInfo["clix"] as? [String: Any],
let urlStr = clix["landing_url"] as? String,
let url = URL(string: urlStr) {
UIApplication.shared.open(url)
}
}
// Optional: when autoRequestPermission is false, show the prompt later:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
// Notify Clix SDK about permission status
Clix.setPushPermissionGranted(granted)
if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } }
}
return result
}
// Optional: override foreground notifications handler and forward to SDK.
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Use SDK default logic (images, analytics) and just forward.
Clix.Notification.handleWillPresent(notification: notification, completionHandler: completionHandler)
// Or, force custom options regardless of SDK logic:
// completionHandler([.banner, .sound, .badge])
}
// Optional: override background notifications handler.
override func application(
_ application: UIApplication,
didReceiveRemoteNotification payload: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
// Wrap the completion to add your own work.
Clix.Notification.handleBackgroundNotification(payload) { result in
// Custom background work (e.g., refresh local cache)
completionHandler(result)
}
}
// Optional: override notification tap event handler.
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// Forward to Clix to keep analytics and deep link behavior consistent.
Clix.Notification.handleDidReceive(response: response, completionHandler: completionHandler)
}
}- Permission requests, device token registration, and event tracking are handled automatically.
- Rich images: for best reliability use a Notification Service Extension. In foreground, the SDK can attach images and re-post the notification when possible.
- Important: Firebase must be configured before calling
super.application()to ensure proper token collection. - Always call super to retain default SDK behavior where indicated.
If you prefer not to inherit from ClixAppDelegate or need more control over your AppDelegate implementation, you can use the static Clix.Notification helpers to handle APNs/FCM wiring and lifecycle callbacks.
import SwiftUI
import UIKit
import Firebase
import FirebaseMessaging
import Clix
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene { WindowGroup { ContentView() } }
}
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
// Configure Firebase
FirebaseApp.configure()
// Initialize Clix SDK
let config = ClixConfig(projectId: "YOUR_PROJECT_ID", apiKey: "YOUR_API_KEY", logLevel: .debug)
Clix.initialize(config: config)
// Set UNUserNotificationCenter delegate if you need to intercept callbacks
UNUserNotificationCenter.current().delegate = self
// Configure push notification handling (disable auto-handling to use custom routing below)
Clix.Notification.configure(
autoRequestPermission: false,
autoHandleLandingURL: false
)
Clix.Notification.handleLaunchOptions(launchOptions)
// Optional: customize foreground presentation and tap handling
Clix.Notification.onMessage { _ in
if #available(iOS 14.0, *) { return [.list, .banner, .sound, .badge] }
else { return [.alert, .sound, .badge] }
}
Clix.Notification.onNotificationOpened { userInfo in
// Custom routing (autoHandleLandingURL is disabled above)
if let clixData = userInfo["clix"] as? [String: Any],
let landingURL = clixData["landing_url"] as? String,
let url = URL(string: landingURL) {
UIApplication.shared.open(url)
}
}
// Optional: handle APNs token registration errors
Clix.Notification.onApnsTokenError { error in
print("APNs registration failed: \(error)")
}
return true
}
// MARK: - APNs/FCM registration
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Clix.Notification.setApnsToken(deviceToken)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
Clix.Notification.handleApnsTokenError(error)
}
// MARK: - Background/foreground receipt
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
Clix.Notification.handleForegroundNotification(userInfo)
}
func application(_ application: UIApplication,
didReceiveRemoteNotification payload: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Clix.Notification.handleBackgroundNotification(payload, completionHandler: completionHandler)
}
// MARK: - UNUserNotificationCenterDelegate (optional)
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// Forward to SDK for consistent image handling and analytics
Clix.Notification.handleWillPresent(notification: notification, completionHandler: completionHandler)
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
Clix.Notification.handleDidReceive(response: response, completionHandler: completionHandler)
}
}Configuration
configure(autoRequestPermission:autoHandleLandingURL:): Configure push notification handlinghandleLaunchOptions(_:): Process launch options when app starts from push
Handler Registration
onMessage(_:): Register handler for foreground messagesonBackgroundMessage(_:): Register handler for background messagesonNotificationOpened(_:): Register handler for notification tapsonApnsTokenError(_:): Register handler for APNs token errors
Delegate Forwarding
handleForegroundNotification(_:): Forward foreground notification to SDKhandleBackgroundNotification(_:completionHandler:): Forward background notification to SDKhandleDidReceive(response:completionHandler:): Forward didReceive delegate to SDKhandleWillPresent(notification:completionHandler:): Forward willPresent delegate to SDKhandleApnsTokenError(_:): Forward APNs token registration error to SDK
Token Management
setApnsToken(_:): Set APNs device tokengetToken(): Get current FCM tokendeleteToken(): Delete FCM token
Permission Management
requestPermission(): Request notification permissionsgetPermissionStatus() async: Get current permission statussetPermissionGranted(_:): Update permission status on server
- The
userInfoparameter is the full dictionary that APNs delivers with a notification; it is equivalent to the Other SDK’snotificationDatamap. - Every Clix notification callback (
onMessage,onBackgroundMessage,onNotificationOpened) forwards this dictionary untouched, so you can read both the"clix"payload and any custom keys you or your backend include. userInfo["clix"]contains the serialized Clix metadata JSON, while any other keys represent app-specific data.- Apple’s APIs and delegate signatures already use the
userInfoname, so keeping that term in your code and documentation keeps everything aligned with the platform vocabulary.
For rich push notifications with images, you can add a Notification Service Extension:
- Add Notification Service Extension target to your app
- Inherit from ClixNotificationServiceExtension
import UserNotifications
import Clix
class NotificationService: ClixNotificationServiceExtension {
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
// Register with your project ID
register(projectId: "YOUR_PROJECT_ID")
// Call super to handle image processing and event tracking
super.didReceive(request, withContentHandler: contentHandler)
}
}A comprehensive sample app is provided in the Samples/BasicApp directory. You can open BasicApp.xcodeproj and run the app on a simulator or device. The sample demonstrates:
- Basic Clix SDK integration
- Push notification handling with Firebase
- User property management
To run the sample:
- Open
Samples/BasicApp/BasicApp.xcodeproj - Update the configuration in
ClixConfiguration.swiftwith your project details - Add your
GoogleService-Info.plistfile - Run the app
All SDK operations can throw ClixError. Always handle potential errors:
do {
try await Clix.setUserId("user123")
} catch {
print("Failed to set user ID: \(error)")
}The SDK is thread-safe and all operations can be called from any thread. Async operations will automatically wait for SDK initialization to complete.
If you notice that push tokens (FCM tokens) are not being collected in the Clix console, check the following:
When using ClixAppDelegate, ensure FirebaseApp.configure() is called before super.application(application, didFinishLaunchingWithOptions: launchOptions):
@main
class AppDelegate: ClixAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ✅ Configure Firebase FIRST
FirebaseApp.configure()
// ✅ Then call super
let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
// Initialize Clix SDK
Task {
let config = ClixConfig(
projectId: "YOUR_PROJECT_ID",
apiKey: "YOUR_API_KEY"
)
await Clix.initialize(config: config)
}
return result
}
}Why this matters: The SDK's super.application internally references the Firebase instance, so Firebase must be initialized first.
If you implement MessagingDelegate, you must forward the FCM token to Clix:
import FirebaseMessaging
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
// ✅ Forward token to Clix SDK
Clix.Notification.messaging(messaging, didReceiveRegistrationToken: fcmToken)
// Your custom logic here
Task {
guard let fcmToken else { return }
// Handle token as needed
}
}
}Avoid setting Messaging.messaging().delegate = self in your AppDelegate when using ClixAppDelegate:
// ❌ DON'T DO THIS
Messaging.messaging().delegate = selfWhy: Clix SDK internally sets Messaging.messaging().delegate = ClixNotification.shared to collect FCM tokens. If you override this, token collection will fail.
If you need custom MessagingDelegate behavior, implement the delegate methods and forward to Clix as shown in step 2.
To verify token collection, enable debug logging:
let config = ClixConfig(
projectId: "YOUR_PROJECT_ID",
apiKey: "YOUR_API_KEY",
logLevel: .debug // Enable debug logs
)
await Clix.initialize(config: config)Look for these log messages in Xcode console:
[Clix] FCM registration token received: ...[Clix] FCM token successfully processed after SDK initialization
If you've disabled automatic permission requests (default is false), you must manually notify Clix when users grant or deny push permissions.
After requesting push permissions in your app, call Clix.setPushPermissionGranted():
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("Permission request error: \(error)")
return
}
// ✅ Notify Clix SDK about permission status
Clix.setPushPermissionGranted(granted)
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}Note: This method is available in SDK version 1.5.0 and later.
If push notifications aren't working, verify:
- ✅ Push Notifications capability is enabled in Xcode project settings
- ✅ Background Modes > Remote notifications is enabled
- ✅
FirebaseApp.configure()is called beforesuper.application(when usingClixAppDelegate) - ✅
Clix.Notification.messaging()is called inMessagingDelegate(if implementing custom delegate) - ✅ NOT setting
Messaging.messaging().delegate = selfwhen usingClixAppDelegate - ✅
Clix.setPushPermissionGranted()is called after requesting permissions (when not using auto-request) - ✅ Testing on a real device (push notifications don't work on iOS 26 simulator)
- ✅ Debug logs show "FCM registration token received" message
The didRegisterForRemoteNotificationsWithDeviceToken method is not called on iOS 26 simulators (particularly iPhone 17 + iOS 26 combination), preventing push token collection during development. This is a confirmed Apple simulator bug affecting multiple push notification SDKs including Firebase and OneSignal.
Status:
- This issue persists as of iOS 26.1/Xcode 26.1.1
- Multiple SDK providers have confirmed this as an Apple simulator-specific limitation
- Real iOS 26 devices work correctly
Workaround: Always test push notification features on real physical devices. This simulator limitation does not affect production apps.
Related Community Reports:
- Firebase iOS SDK #15327 - Messaging.messaging().token returns error in iOS 26 Simulator
- Firebase iOS SDK #15315 - didRegisterForRemoteNotificationsWithDeviceToken not called on iPhone 17 + iOS 26 simulator
Note for UISceneDelegate Users: If migrating to UISceneDelegate, AppDelegate must still be maintained for remote notification handling as there's no scene delegate equivalent for APNs token registration methods.
If you continue to experience issues:
- Enable debug logging (
.debuglog level) - Check Xcode console for Clix log messages
- Verify your device appears in the Clix console Users page
- Check if
push_tokenfield is populated for your device - Create an issue on GitHub with logs and configuration details
This project is licensed under the MIT License with Custom Restrictions. See the LICENSE file for details.
See the full release history and changes in the CHANGELOG.md file.
We welcome contributions! Please read the CONTRIBUTING.md guide before submitting issues or pull requests.