iOS SDK
Native Swift SDK for real-time push notifications via APNs, PushKit VoIP and the Abrova real-time connection, with no Firebase dependency.
Background push wakes the app even when killed or terminated
The Abrova real-time connection delivers instantly in the foreground
Connection settings are fetched automatically
Foreground UI driven by the SDK
Full control over your push infrastructure
Subscribe users to topics or target by user ID
Installation#
AbrovaPush installs with Swift Package Manager:
- In Xcode: File → Add Package Dependencies → enter the repository URL:
https://github.com/Abrova-co/abrova-push-ios-sdk.git
- Choose version
0.1.16or later and add theAbrovaPushlibrary to your app target.
Or, in a Package.swift:
dependencies: [
.package(url: "https://github.com/Abrova-co/abrova-push-ios-sdk.git", from: "0.1.16")
]Import the module as AbrovaPush:
import AbrovaPush
Required Capabilities
In your Xcode project, enable:
- Push Notifications
- Background Modes:
- Voice over IP
- Background fetch
- Remote notifications
APNs Key
Background delivery (APNs, and VoIP if you enable it) uses your project's APNs auth key — the same .p8 key covers both regular and VoIP pushes:
- Apple Developer Portal → Certificates, Identifiers & Profiles → Keys
- Create a key with Apple Push Notifications service (APNs) enabled and download the
.p8file - In Abrova Console → Abrova Push → Settings → iOS Push (APNs) Configuration, enter the Key ID, Team ID and paste the
.p8key content
Quick Start#
Initialize AbrovaPush in your AppDelegate:
import UIKit
import AbrovaPush
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Only apiKey is required — connection settings are fetched
// automatically during registration.
let config = AbrovaPushConfig(apiKey: "ab_live_your_api_key")
AbrovaPush.shared.initialize(config: config)
AbrovaPush.shared.delegate = self
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Forward the APNs token so the server can reach the device in background.
AbrovaPush.shared.setAPNsToken(deviceToken)
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Open the Abrova real-time connection while the app is in foreground.
AbrovaPush.shared.connect()
}
func applicationWillResignActive(_ application: UIApplication) {
// Close the real-time connection when going to background.
// VoIP pushes will still wake the app.
AbrovaPush.shared.disconnect()
}
}Register the Device#
import UIKit
import AbrovaPush
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Register on every launch: it keeps the device's token, app version
// and last-seen time current on the server.
AbrovaPush.shared.register(
userId: "user_123",
metadata: [
"tier": "premium"
]
)
}
}Implement the Delegate#
extension AppDelegate: AbrovaPushDelegate {
func abrovaPush(_ abrovaPush: AbrovaPush, didReceiveMessage message: AbrovaPushMessage) {
// New push notification — message.title, message.body, message.data
}
func abrovaPush(_ abrovaPush: AbrovaPush, didChangeConnectionState connected: Bool) {
// Real-time connection state changed
}
func abrovaPush(_ abrovaPush: AbrovaPush, didRegisterWithDeviceId deviceId: String) {
// Device registered with the Abrova backend
}
func abrovaPush(_ abrovaPush: AbrovaPush, didReceiveVoIPToken token: String) {
// VoIP token for backend delivery
}
func abrovaPush(_ abrovaPush: AbrovaPush, didFailWithError error: Error) {
// Connection or registration error
}
}How Push Delivery Works#
┌────────────────────────────────────────────────────────────┐ │ App State │ ├─────────────────┬─────────────────┬────────────────────────┤ │ Foreground │ Background │ Killed/Terminated │ ├─────────────────┼─────────────────┼────────────────────────┤ │ Abrova │ APNs │ APNs │ │ (real-time) │ (VoIP optional) │ (VoIP optional) │ └─────────────────┴─────────────────┴────────────────────────┘
- Foreground: the Abrova real-time connection delivers messages instantly with no APNs latency.
- Background / Killed: APNs delivers the notification. With
usePushKitenabled, messages whosedata.typeis"voip_call"go out as VoIP pushes instead (calling apps only, see step 8).
VoIP push is for calling apps. Apple requires every VoIP push to report an incoming call through CallKit, so enable usePushKit only if your app makes calls.
Topics#
Subscribe users to topics so you can broadcast notifications to interest groups:
AbrovaPush.shared.subscribeTopic("news")
// Later, when the user opts out
AbrovaPush.shared.unsubscribeTopic("news")User Identification#
// Set user ID after login
AbrovaPush.shared.setUserId("user_123")
// Clear user ID after logout — also detaches the user on the server,
// so this device stops receiving the previous user's notifications
AbrovaPush.shared.clearUserId()Backend VoIP Push Format#
Abrova sends a VoIP push (instead of a regular APNs push) only when the message's data.type is "voip_call" and the device registered a VoIP token (usePushKit: true). The VoIP payload Abrova builds has this shape:
{
"title": "New Message",
"body": "You have a new message",
"data": {
"type": "voip_call",
"screen": "chat",
"chatId": "123"
}
}The data dictionary is delivered verbatim to didReceiveMessage so you can drive in-app navigation.
Configuration Options#
let config = AbrovaPushConfig(
apiKey: "ab_live_your_api_key",
usePushKit: false,
useAPNs: true,
showNotificationInForeground: false,
autoConnect: true,
appGroup: "group.com.yourcompany.yourapp",
autoRefresh: true
)| Option | Type | Required | Description |
|---|---|---|---|
| apiKey | String | Yes | Your Abrova Push API key |
| usePushKit | Bool | No | Enable VoIP pushes, for calling apps only (default: false) |
| useAPNs | Bool | No | Enable standard APNs (default: true) |
| autoConnect | Bool | No | Auto-connect when app enters foreground (default: true) |
| autoReconnect | Bool | No | Reconnect with exponential backoff (default: true) |
| showNotificationInForeground | Bool | No | Show notification banner when app is in foreground (default: false) |
| appGroup | String | No | App Group shared with a Notification Service Extension, required only for background delivery confirmation (see Delivery Confirmation below) |
| autoRefresh | Bool | No | On initialize, silently refresh a previously registered device at most once every 24h, or sooner when the app version, SDK version, push token or user changed. Never prompts for permission (default: true) |
Connection settings are fetched automatically. There is nothing to configure.
You can also build the config fluently with AbrovaPushConfig.builder(apiKey:), for example .usePushKit(false).autoRefresh(true).appGroup("group.com.yourcompany.yourapp").build(). The installed SDK version is available as AbrovaPush.sdkVersion.
Delivery Confirmation (Notification Service Extension)#
Optional. Set this up only if you want to know that background notifications actually arrived. Everything else in the SDK works without it.
APNs tells the server it accepted a notification for delivery — it never confirms one reached the device. The SDK confirms delivery itself for messages the app sees:
| How the message arrived | Confirmed by |
|---|---|
| Abrova real-time connection (app in foreground) | The SDK, no setup |
| APNs, app in foreground | The SDK, when it is the `UNUserNotificationCenter` delegate. If you use your own delegate, call `AbrovaPush.shared.handleRemoteNotification(userInfo:)` |
| APNs, user taps the notification | The SDK, no setup |
| APNs, app in background or not running | Your Notification Service Extension (this step) |
iOS does not run your app when a notification arrives in the background, so
without an extension those deliveries stay sent. A Notification Service
Extension is the only way iOS lets an app observe a real arrival: the system
runs the extension before the notification is displayed, even when your app is
not running. Each message is confirmed once — the app and the extension share
the list of confirmed messages through the App Group.
If you handle silent pushes, forward them too:
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
AbrovaPush.shared.handleRemoteNotification(userInfo: userInfo)
completionHandler(.noData)
}An extension is a separate process, so it must be its own target — Apple provides no alternative. Every push SDK that reports iOS delivery works this way. Skip this section entirely if you do not need delivery confirmation.
1. Add the extension target
In Xcode: File → New → Target → Notification Service Extension.
| Field | Value |
|---|---|
| Product Name | NotificationService (any name works) |
| Language | Swift |
| Project | Your app project |
| Embed in Application | Your app target — if this is not set the extension never ships, and fails silently |
When Xcode asks "Activate scheme?", choose Don't Activate so your app stays the active scheme.
Then set the extension's iOS Deployment Target to match your app. Xcode defaults new targets to the latest iOS version, which would drop support for every older device.
2. Enable the same App Group on both targets
Select each target → Signing & Capabilities → + Capability → App Groups, and
add the same group to both your app and the extension, for example
group.com.yourcompany.yourapp.
The extension runs in its own process and cannot read your app's storage. The App Group is the shared container the SDK uses to hand the device identity to the extension.
3. Add the SDK to the extension
The extension needs the SDK too. In Xcode, select the extension target → General → Frameworks and Libraries → + and add the AbrovaPush library from the package you already added. The extension only uses AbrovaPushServiceExtension, which never touches UIApplication.
4. Pass the App Group to the SDK
AbrovaPush.shared.initialize(
config: AbrovaPushConfig(
apiKey: "ab_live_your_api_key",
appGroup: "group.com.yourcompany.yourapp"
)
)5. Report delivery from the extension
Replace the generated NotificationService.swift with:
import UserNotifications
import AbrovaPush
class NotificationService: UNNotificationServiceExtension {
private static let appGroup = "group.com.yourcompany.yourapp"
private static let apiKey = "ab_live_your_api_key"
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
AbrovaPushServiceExtension.didReceive(
request,
apiKey: NotificationService.apiKey,
appGroup: NotificationService.appGroup
)
contentHandler(bestAttemptContent ?? request.content)
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}The report is fire-and-forget and never delays contentHandler. Extensions get a
short execution budget, so a missed report costs a delivery statistic — never the
notification itself.
Xcode's generated extension appends [modified] to every notification title.
If you leave it in, your users will see it.
Verifying it works
Send a notification and check the receipt for that device: it should move from
sent to delivered. See Delivery Receipts.
Troubleshooting
| Symptom | Cause |
|---|---|
| Receipt stays `sent` | App Group string differs between the two targets or the config — it must match exactly in all three places. This fails silently. |
| Receipt stays `sent` on first launch | The device must register once after you grant notification permission, so the SDK can write its identity into the shared container. |
| `No such module AbrovaPush` in the extension | The extension target has no dependency — add the `AbrovaPush` library to the extension target, not just to the app. |
| `Cycle inside <YourApp>` | Build phase ordering. In the app target, move `Embed Foundation Extensions` before any script phase that outputs the `.app`. |
| Extension never runs | `Embed in Application` was not set when the target was created, so the extension is not inside the build. |
API Reference#
Core methods of AbrovaPush.shared and supporting types (the SDK also exposes in-app message, inbox and A/B testing APIs).
AbrovaPush — Initialization & Lifecycle
| Option | Type | Required | Description |
|---|---|---|---|
| initialize(config:) | - | - | Initialize the SDK with an AbrovaPushConfig |
| connect() | - | - | Open the Abrova real-time connection (foreground) |
| disconnect() | - | - | Close the real-time connection (VoIP push still works) |
| register(userId:metadata:) | - | - | Register the device with the Abrova backend. Call on every launch |
| setAPNsToken(_:) | - | - | Forward the APNs device token |
| handleRemoteNotification(userInfo:) | - | - | Hand a received notification to the SDK (own notification delegate or silent pushes) |
| unregister() | - | - | Disconnect and detach the user on the server |
| sdkVersion | - | - | Static: the installed SDK version |
User & Topics
| Option | Type | Required | Description |
|---|---|---|---|
| setUserId(_:) | - | - | Set the current user identifier |
| clearUserId() | - | - | Clear the user identifier and detach the user on the server (e.g. on logout) |
| subscribeTopic(_:) | - | - | Subscribe the device to a topic |
| unsubscribeTopic(_:) | - | - | Unsubscribe from a topic |
AbrovaPushDelegate callbacks
| Option | Type | Required | Description |
|---|---|---|---|
| didReceiveMessage | - | - | A push notification was received |
| didChangeConnectionState | - | - | Real-time connection opened or closed |
| didRegisterWithDeviceId | - | - | Registration completed; deviceId returned by the backend |
| didReceiveVoIPToken | - | - | PushKit issued a new VoIP token |
| didFailWithError | - | - | Connection or registration failed |
Requirements#
- iOS 13.0+
- Xcode 14+
- Swift 5.7+
Best Practices#
Call AbrovaPush.shared.initialize() in didFinishLaunchingWithOptions so the SDK is ready before any view controller
Use applicationDidBecomeActive / applicationWillResignActive to manage the real-time connection — VoIP pushes still work in background
Call setUserId() once authentication completes so notifications target the right account
Topics suit large audiences; use them for newsletters and app-wide announcements
The data dictionary is your deep-link contract — keep it small and versioned