The Pushix Swift package registers iPhones and iPads with Pushix, shows notifications while the app is open, tells your app when one is tapped, and shows in-app messages.
This guide is for native Swift and Objective-C apps. Flutter apps use the Flutter SDK instead.
Requirements
- Xcode 15.2 or newer (Firebase 11 needs it). The SDK works with Firebase 10 and 11; on older Xcode, pin Firebase 10 in your project.
- iOS 13.0 or newer
- A paid Apple Developer account (push needs an APNs key) and a real iPhone for testing
- A Firebase project with your iOS app in it, the same project whose service account you upload to Pushix. iPhone notifications travel from Pushix through Firebase to Apple.
1. Firebase and the APNs key
Four one-time steps:
Add the app to Firebase. In the Firebase console, open your project → Project settings → Your apps → Add app → iOS, with your app's bundle id. Download GoogleService-Info.plist and drag it into your app target in Xcode (tick "Copy items if needed" and your app target).
APNs key in Firebase. In the Apple Developer site create a key with Apple Push Notifications service (APNs) and download the .p8 file. In Firebase → Project settings → Cloud Messaging → Apple app configuration, upload it with its Key ID and your Team ID. One key works for all your apps.
Service account in Pushix. If you haven't yet: Firebase → Project settings → Service accounts → Generate new private key, then in the dashboard App settings → Channels → Android and iOS → upload the JSON file. It must be from the same project as GoogleService-Info.plist, or every send fails with SENDER_ID_MISMATCH.
Capabilities. In Xcode select your app target → Signing & Capabilities → + Capability, and add Push Notifications and Background Modes with Remote notifications ticked.
2. Add the package
Copy the sdks/ios folder next to your app as pushix-ios (or put it in its own private Git repository). If the app already uses another push SDK, remove its package or pod: two push SDKs in one app fight over the notification delegate.
Swift Package Manager — in Xcode: File → Add Package Dependencies → Add Local… and pick the pushix-ios folder (or paste your Git repository URL). Add the Pushix library to your app target. Xcode fetches Firebase automatically.
In a Package.swift of your own:
dependencies: [
.package(url: "https://github.com/you/pushix-ios.git", from: "1.0.0"),
],
targets: [
.target(name: "YourApp", dependencies: [.product(name: "Pushix", package: "pushix-ios")]),
]CocoaPods — in your Podfile, then run pod install:
platform :ios, '13.0'
use_frameworks! :linkage => :static
target 'YourApp' do
pod 'Pushix', :path => '../pushix-ios'
end
# For pictures and buttons: the Notification Service Extension (see below)
target 'NotificationService' do
pod 'Pushix/Extension', :path => '../pushix-ios'
endWith CocoaPods both targets use import Pushix; with Swift Package Manager the extension uses import PushixExtension.
3. Initialize
Call initialize in application(_:didFinishLaunchingWithOptions:). That is where iOS needs the notification delegate to be set, so taps that launch the app are received.
UIKit:
import UIKit
import Pushix
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Pushix.initialize("YOUR_APP_ID", serverUrl: "https://pushix.in", launchOptions: launchOptions)
Pushix.Notifications.addClickListener { event in
let data = event.notification.additionalData ?? [:]
// route to a screen, e.g. data["screen"] as? String == "orders"
}
return true
}
}SwiftUI — add an app delegate with @UIApplicationDelegateAdaptor:
import SwiftUI
import Pushix
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
Pushix.initialize("YOUR_APP_ID", serverUrl: "https://pushix.in", launchOptions: launchOptions)
return true
}
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup { ContentView() }
}
}Pushix calls FirebaseApp.configure() for you if your app hasn't. If you use other Firebase features, call FirebaseApp.configure() yourself before Pushix.initialize (Firebase crashes if it is configured twice).
Then ask for permission at a sensible moment:
Pushix.Notifications.requestPermission({ accepted in
print("Notifications allowed: \(accepted)")
}, fallbackToSettings: true)When the user logs in to your app, link the device to your user id so your backend can target them:
Pushix.login(String(user.id)) // on login
Pushix.logout() // on logout (also clears tags)That's it. Run the app on an iPhone, allow notifications, and the device appears in the dashboard under Subscribers, marked iOS.
The SDK can be called from any thread; it does its work on the main thread and calls your listeners on the main thread.
API overview
| Call | What it does |
|---|---|
Pushix.initialize(appId, serverUrl: ..., launchOptions: launchOptions) | Starts the SDK, in didFinishLaunching. Pushix.initialize(appId, withLaunchOptions: launchOptions) does the same with the server URL from Pushix_server_url in Info.plist |
Pushix.Debug.setLogLevel(.verbose) | How much the SDK writes to the Xcode console (.none … .verbose) |
Pushix.login(id) / Pushix.logout() | Links the device to your user id / unlinks it (also clears tags) |
Pushix.setConsentRequired(true) / Pushix.setConsentGiven(true) | Registers only after the user agrees |
Pushix.Notifications.requestPermission({ accepted in }, fallbackToSettings: true) | Asks for notification permission |
Pushix.Notifications.permission / canRequestPermission / permissionNative | Permission state (permissionNative is Apple's UNAuthorizationStatus) |
Pushix.Notifications.addClickListener / addForegroundLifecycleListener / addPermissionObserver / addSilentDataListener and their remove… | Taps, notifications arriving while the app is open, permission changes, data-only messages |
Pushix.Notifications.clearAll() / setBadgeCount(n) / clearBadge() | Notification Center and the app icon badge |
Pushix.User.addTag(key:value:) / addTags / removeTag / removeTags / getTags() / setLanguage | Tags and message language |
Pushix.User.externalId / Pushix.User.playerId | The id passed to login; this device's player id |
Pushix.User.pushSubscription.id / .token / .optedIn / optIn() / optOut() / addObserver | Player id, FCM token and opt-in state of this device |
Pushix.InAppMessages.addTrigger(_:withValue:) / addTriggers / removeTrigger / removeTriggers / clearTriggers / paused | In-app message triggers, and holding messages back |
Pushix.InAppMessages.addClickListener / addLifecycleListener | In-app message events |
class NotificationService: PushixNotificationServiceExtension {} | Pictures and buttons (see below) |
Listener protocols and their methods:
| Protocol | Method |
|---|---|
PushixNotificationClickListener | onClick(event:) |
PushixNotificationLifecycleListener | onWillDisplay(event:) |
PushixNotificationPermissionObserver | onNotificationPermissionDidChange(_:) |
PushixPushSubscriptionObserver | onPushSubscriptionDidChange(state:) |
PushixSilentDataListener | onSilentData(notification:) |
PushixInAppMessageClickListener | onClick(event:) |
PushixInAppMessageLifecycleListener | onWillDisplay(event:), onDidDisplay(event:), onWillDismiss(event:), onDidDismiss(event:), all optional |
Every listener also has a closure form, e.g. Pushix.Notifications.addClickListener { event in … }, which returns the listener object for remove….
Permission
Pushix.Notifications.permission // Bool: allowed (also true for provisional)
Pushix.Notifications.canRequestPermission // true until the system dialog has been shown
Pushix.Notifications.addPermissionObserver { granted in /* update your UI */ }- The device registers right away, before permission, with
notification_types0. It becomes a subscriber when the user allows notifications. - iOS shows the system dialog only once. With
fallbackToSettings: true, a later call shows an alert that opens the app's notification settings instead. - Permission is re-read whenever the app becomes active, so turning notifications on in Settings is noticed.
Tags and users
Tags are key–value strings used for segments, filters and {{ personalisation }}.
Pushix.User.addTags(["level": 7, "plan": "gold", "last_bonus": Int(Date().timeIntervalSince1970)])
Pushix.User.addTag(key: "city", value: "Delhi")
Pushix.User.removeTag("plan")
let tags = Pushix.User.getTags() // tags set from this device
Pushix.User.setLanguage("bn") // pick the "bn" translation of messagesNumbers and booleans are sent as text. Your backend can also set tags by user id without the app: PUT /api/v1/apps/{app_id}/users/{external_user_id}. Changes made while offline are saved on the phone and sent the next time it can reach your server.
Pushix.User.pushSubscription.id // player id, nil until registered
Pushix.User.pushSubscription.optedIn // allowed and not opted out
Pushix.User.pushSubscription.optOut() // stop push to this device without touching the permission
Pushix.User.pushSubscription.addObserver { state in print(state.current.id ?? "") }Handling taps
Pushix.Notifications.addClickListener { event in
let n = event.notification
print(n.notificationId) // id from the REST API
print(n.additionalData ?? [:]) // your "data" object
print(n.launchURL ?? "") // your "url"
print(event.result.actionId ?? "body") // button id, or nil for the notification itself
print(event.result.url ?? "") // the button's url, or launchURL
}- Taps that open the app from closed are delivered as soon as you add the first click listener, so add it in
didFinishLaunching. http://andhttps://URLs (the messageurl, or a tapped button'surl) open in Safari automatically. To handle them yourself, addPushix_suppress_launch_urls= YES to Info.plist.- Deep links (
myshop://orders) are not opened automatically; readevent.result.urland navigate yourself. - Each tap is reported to the server and shows up as "Clicked" in the dashboard.
While the app is open
Notifications are shown as a banner even when the app is in the foreground (with sound and badge when the message has them). To hide or delay one:
Pushix.Notifications.addForegroundLifecycleListener { event in
if currentScreen == "orders" {
event.preventDefault() // don't show it
// event.notification.display() // or show it later
}
}A notification you preventDefault() stays hidden if you don't call display() within 25 seconds; calling display() after that shows it again as a new notification.
Buttons
Send "buttons": [{"id": "claim", "text": "Claim", "url": "https://…"}] as for Android. With the Notification Service Extension added (see "Pictures and buttons" below), iPhones show them too: the extension registers a notification category for the buttons and puts it on the notification. Each button opens the app. The tapped button's id arrives as event.result.actionId and its url as event.result.url (http(s) links also open in Safari, see Handling taps). Without the extension iOS shows no buttons, only the notification.
ios_category is only needed for categories of your own, e.g. with actions that don't open the app or that ask for text. Register the category once in didFinishLaunching:
let claim = UNNotificationAction(identifier: "claim", title: "Claim", options: [.foreground])
let later = UNNotificationAction(identifier: "later", title: "Later", options: [])
UNUserNotificationCenter.current().setNotificationCategories([
UNNotificationCategory(identifier: "OFFER_ACTIONS", actions: [claim, later], intentIdentifiers: [])
])and send "ios_category": "OFFER_ACTIONS" (composer → iOS options → Category) with the same button ids in buttons, so the SDK can find each button's URL. When ios_category is set, Pushix leaves the buttons to your category. Categories Pushix creates are named pushix_…; your own categories are never changed, and only the 20 most recently used Pushix ones are kept.
Badges
Send ios_badgeType Increase or SetTo with ios_badgeCount. Pushix keeps each iPhone's count on the server.
- The SDK clears the number on the app icon when a session starts (the app opens, or comes back after 30+ seconds); the server resets its count at the same moment. On iOS 15 and older, iOS also removes the app's notifications from Notification Center when the badge is cleared.
- To keep the badge, add
Pushix_disable_badge_clearing= YES to Info.plist. The SDK then sends the badge number with each session, so the server's count stays in step and the nextIncreaseadds to it. - Set the number yourself with
Pushix.Notifications.setBadgeCount(3)orclearBadge(). Both also tell the server, soIncreasecontinues from that number.
Pictures and buttons (Notification Service Extension)
iOS only shows a picture (big_picture or ios_attachments) when the app has a Notification Service Extension that downloads it, and only shows buttons from a registered category. The Pushix extension does both. Pushix marks messages with a picture, or with buttons and no ios_category, as mutable-content automatically, so the extension runs for them.
- In Xcode: File → New → Target → Notification Service Extension, name it
NotificationService. Set its deployment target to the same iOS version as the app. - Add the PushixExtension library to the new target (target → General → Frameworks and Libraries → +). With CocoaPods use
pod 'Pushix/Extension'as shown above. - Replace the contents of
NotificationService.swiftwith:
import PushixExtension // CocoaPods: import Pushix
class NotificationService: PushixNotificationServiceExtension {}The extension downloads the picture (waiting up to 20 seconds) and attaches it; if the download fails or is too slow the notification is shown without it. At the same time it registers the buttons' category and waits (up to 3 seconds) until iOS has it, because a notification that names a category iOS doesn't know yet shows no buttons. It doesn't use Firebase. To change a notification before it is shown, override willDeliver(_:for:); to wait a different time for the picture, override imageDownloadTimeout.
In-app messages
Create them in the dashboard under In-app messages, with iOS ticked under platforms: a pop-up, card, top or bottom banner, or full screen, with your colours, picture and up to three buttons. The SDK downloads the ones meant for this device each time a session starts and shows them by their trigger:
| Trigger | Shows |
|---|---|
| When the app opens | On each new session (launch, or back after 30+ seconds) |
| From the Nth session | When the app opens and the device has had at least N sessions |
| After some time in the app | N seconds into the session |
| When my code sets a trigger | When your code calls addTrigger with a matching key and value |
Pushix.InAppMessages.addTrigger("cart_value", withValue: "1200")
Pushix.InAppMessages.paused = true // e.g. during a game round
Pushix.InAppMessages.paused = false
Pushix.InAppMessages.addClickListener { event in
// event.result.actionId is the button id; event.result.url its link
if event.result.action == "deep_link", event.result.url == "myshop://account" {
router.showKyc()
}
}Messages appear in their own window above your app, inside the safe area, and keep the colours you chose in both light and dark mode. Banners and cards leave the rest of the screen usable. Frequency rules (show at most N times, wait between showings) are kept on the device. "Open a link" buttons open Safari; "Deep link" buttons are handed to your click listener (or opened with the system if you have none). Seen, clicked and closed counts appear in the dashboard and in Analytics.
For will/did display and dismiss events, implement PushixInAppMessageLifecycleListener (all methods optional) and pass it to Pushix.InAppMessages.addLifecycleListener(_:). To turn in-app messages off completely, add Pushix_disable_in_app_messages = YES to Info.plist.
Using your own notification delegate
Pushix needs to see notification events to show notifications in the foreground and report taps. There are two ways to set that up.
Automatic (default). Pushix.initialize makes Pushix the UNUserNotificationCenter delegate. If your app set its own delegate before calling initialize, Pushix forwards everything that isn't a Pushix notification to it (foreground presentation, taps and "notification settings"), so your own local notifications keep working. Set your delegate first, then call initialize.
Manual. If your delegate must stay the delegate (or is set later, e.g. by another SDK), add Pushix_manual_notification_delegate = YES to Info.plist and forward the two calls:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if Pushix.Notifications.handleWillPresent(notification, completionHandler: completionHandler) { return }
completionHandler([.banner, .list, .sound]) // your own notifications
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
if Pushix.Notifications.handleNotificationResponse(response, completionHandler: completionHandler) { return }
// handle your own notifications here
completionHandler()
}
}Both methods return true for Pushix notifications and then call completionHandler themselves. If something replaces the delegate after initialize without forwarding, Pushix notifications are not shown in the foreground or reported as clicked, and the SDK logs a warning.
APNs token. Firebase picks up the APNs token by itself through method swizzling. If you turned that off (FirebaseAppDelegateProxyEnabled = NO), pass the token on:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Pushix.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}Data-only messages
Messages sent with content_available: true and no contents don't show anything. iOS hands them to your app delegate; pass them to Pushix and listen for them:
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Pushix.Notifications.handleSilentNotification(userInfo)
completionHandler(.newData)
}
Pushix.Notifications.addSilentDataListener { n in
if n.additionalData?["refresh"] as? String == "balance" { reloadBalance() }
}They need Background Modes → Remote notifications, and iOS limits how often they wake an app.
Consent
To register only after the user agrees (GDPR):
Pushix.setConsentRequired(true) // before initialize; remembered
Pushix.initialize(...)
// later, when the user agrees:
Pushix.setConsentGiven(true)Firebase itself may create an FCM token at start-up; to stop that too, add FirebaseMessagingAutoInitEnabled = NO to Info.plist.
Info.plist keys
| Key | Type | Effect |
|---|---|---|
Pushix_server_url | String | Server URL for Pushix.initialize(_:withLaunchOptions:) |
Pushix_suppress_launch_urls | Boolean | Don't open http(s) notification links in Safari; handle event.result.url yourself |
Pushix_disable_badge_clearing | Boolean | Keep the app icon badge when a session starts |
Pushix_manual_notification_delegate | Boolean | Don't become the notification delegate; forward calls yourself |
Pushix_disable_in_app_messages | Boolean | Don't download or show in-app messages |
Example app
sdks/ios/Example is a small SwiftUI app with the setup above, buttons for permission, login, tags, opt-out and in-app triggers, and a Notification Service Extension. Generate its Xcode project with XcodeGen (cd Example && xcodegen), add your GoogleService-Info.plist, set your team and bundle id, and run it on an iPhone.
Testing
- Run the app on a real iPhone and allow notifications. The simulator only receives pushes on recent Xcode with an Apple silicon or T2 Mac.
- Turn on
Pushix.Debug.setLogLevel(.verbose)and look for "Registered device …" in the Xcode console. - In the dashboard, open Subscribers, click your device and tick Test device.
- New message → Send to test devices.
Troubleshooting
| Problem | Fix |
|---|---|
| Device never appears in Subscribers | Check serverUrl (must be reachable from the phone over HTTPS) and the App ID. Turn on Pushix.Debug.setLogLevel(.verbose) and read the Xcode console. |
| "No APNs token yet" in the log | Add the Push Notifications capability, check the bundle id matches the iOS app in Firebase, and test on a real device. |
| "GoogleService-Info.plist is missing" | Add the file from Firebase to the app target (Target Membership ticked). |
| Every send fails with SENDER_ID_MISMATCH | The service account in the dashboard is from a different Firebase project than GoogleService-Info.plist. |
| Registers but nothing arrives | Upload the APNs key in Firebase → Cloud Messaging. Firebase reports this as an authentication error in the message's error. |
| Nothing shows while the app is open, and taps aren't counted | Something replaced the notification delegate after Pushix.initialize. Set yours before, or forward to Pushix (see "Using your own notification delegate"). |
| The tap that launched the app is missing | Call Pushix.initialize in didFinishLaunching (not later), and add the click listener there too. |
| Crash "Default app has already been configured" | Call FirebaseApp.configure() before Pushix.initialize, not after. |
| No picture | Add the Notification Service Extension, and check its deployment target isn't higher than the phone's iOS version. |
| No buttons | Add the Notification Service Extension (the SDK logs a warning when buttons arrive without it). With ios_category, register that category in the app (see Buttons). Long-press or swipe down the notification to see its buttons. |
| Badge disappears when the app opens | That's the default. Add Pushix_disable_badge_clearing to Info.plist. |
| In-app message never shows | Check its status is Live, iOS is ticked under platforms, and its frequency hasn't been used up on this device. Messages are fetched when a session starts. |
Send your first notification
Create an app in Pushix to get your App ID and REST API key, then follow this guide. The free plan needs no card.