Abrova
☰ Docs menu · iOS
Coming soon. Abrova Push is coming soon. This page describes how it will work; it can’t be used yet.

iOS SDK

Native Swift SDK for real-time push notifications via APNs, PushKit VoIP and the Abrova real-time connection, with no Firebase dependency.

iOS 0.1.16iOS 13+Swift 5.7+Last updated: 27 Oct 2026
PushKit VoIP Delivery

Background push wakes the app even when killed or terminated

Real-time Connection

The Abrova real-time connection delivers instantly in the foreground

Automatic Setup

Connection settings are fetched automatically

Local Notifications

Foreground UI driven by the SDK

No Google/Firebase

Full control over your push infrastructure

Topics & User IDs

Subscribe users to topics or target by user ID

Installation#

AbrovaPush installs with Swift Package Manager:

  1. In Xcode: File → Add Package Dependencies → enter the repository URL:
https://github.com/Abrova-co/abrova-push-ios-sdk.git
  1. Choose version 0.1.16 or later and add the AbrovaPush library to your app target.

Or, in a Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/Abrova-co/abrova-push-ios-sdk.git", from: "0.1.16")
]

Import the module as AbrovaPush:

swift
import AbrovaPush

Required Capabilities

In your Xcode project, enable:

  1. Push Notifications
  2. 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:

  1. Apple Developer Portal → Certificates, Identifiers & Profiles → Keys
  2. Create a key with Apple Push Notifications service (APNs) enabled and download the .p8 file
  3. In Abrova Console → Abrova Push → Settings → iOS Push (APNs) Configuration, enter the Key ID, Team ID and paste the .p8 key content

Quick Start#

Initialize AbrovaPush in your AppDelegate:

swift
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#

swift
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#

swift
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)     │
└─────────────────┴─────────────────┴────────────────────────┘
  1. Foreground: the Abrova real-time connection delivers messages instantly with no APNs latency.
  2. Background / Killed: APNs delivers the notification. With usePushKit enabled, messages whose data.type is "voip_call" go out as VoIP pushes instead (calling apps only, see step 8).
Apple VoIP Push Requirement.

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:

swift
AbrovaPush.shared.subscribeTopic("news")

// Later, when the user opts out
AbrovaPush.shared.unsubscribeTopic("news")

User Identification#

swift
// 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:

json
{
  "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#

swift
let config = AbrovaPushConfig(
    apiKey: "ab_live_your_api_key",
    usePushKit: false,
    useAPNs: true,
    showNotificationInForeground: false,
    autoConnect: true,
    appGroup: "group.com.yourcompany.yourapp",
    autoRefresh: true
)
OptionTypeRequiredDescription
apiKeyStringYesYour Abrova Push API key
usePushKitBoolNoEnable VoIP pushes, for calling apps only (default: false)
useAPNsBoolNoEnable standard APNs (default: true)
autoConnectBoolNoAuto-connect when app enters foreground (default: true)
autoReconnectBoolNoReconnect with exponential backoff (default: true)
showNotificationInForegroundBoolNoShow notification banner when app is in foreground (default: false)
appGroupStringNoApp Group shared with a Notification Service Extension, required only for background delivery confirmation (see Delivery Confirmation below)
autoRefreshBoolNoOn 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)
Automatic connection setup.

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 arrivedConfirmed by
Abrova real-time connection (app in foreground)The SDK, no setup
APNs, app in foregroundThe SDK, when it is the `UNUserNotificationCenter` delegate. If you use your own delegate, call `AbrovaPush.shared.handleRemoteNotification(userInfo:)`
APNs, user taps the notificationThe SDK, no setup
APNs, app in background or not runningYour 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:

swift
func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    AbrovaPush.shared.handleRemoteNotification(userInfo: userInfo)
    completionHandler(.noData)
}
This is the one step that requires an Xcode change.

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.

FieldValue
Product NameNotificationService (any name works)
LanguageSwift
ProjectYour app project
Embed in ApplicationYour 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

swift
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:

swift
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.

Delete the template code.

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

SymptomCause
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 launchThe 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 extensionThe 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

OptionTypeRequiredDescription
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

OptionTypeRequiredDescription
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

OptionTypeRequiredDescription
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#

Initialize in AppDelegate

Call AbrovaPush.shared.initialize() in didFinishLaunchingWithOptions so the SDK is ready before any view controller

Connect on Active, Disconnect on Resign

Use applicationDidBecomeActive / applicationWillResignActive to manage the real-time connection — VoIP pushes still work in background

Set User ID After Login

Call setUserId() once authentication completes so notifications target the right account

Use Topics for Broadcasts

Topics suit large audiences; use them for newsletters and app-wide announcements

Handle data Payload in didReceiveMessage

The data dictionary is your deep-link contract — keep it small and versioned

Was this page helpful?
Report a problem