Documentation / iOS SDK

SDKs · Swift

iOS SDK

Native push for your Swift app. Register with APNs, manage permission and opt-out, display rich notifications and report engagement.

On this page

Before you start

  • An iOS 15+ app, Xcode and a Swift tools 5.9+ package environment.
  • An Apple Developer team that can sign your app and its notification extension.
  • The iOS platform enabled in the dashboard, with your exact bundle ID and APNs credentials.
  • Your public app key from App Settings → Keys & IDs.

The SDK works with SwiftUI and UIKit and has no third-party runtime dependency.

1. Add the Swift package

In Xcode, choose File → Add Package Dependencies and enter:

Package repository
https://github.com/notirabbit/notirabbit-ios.git

The repository is private; your GitHub account in Xcode needs read access. Select the main branch or pin a reviewed commit revision. See release status for versioning.

  • Add Notirabbit to your main app target.
  • Add NotirabbitNotificationService to a Notification Service Extension target.

The repository name is notirabbit-ios; Swift imports use the product names above.

2. Configure both targets

  1. Enable Push Notifications on the main app target.
  2. Create a Notification Service Extension, embed it in the app and sign both targets with the same team. Set the extension’s minimum iOS version to 15.
  3. Enable the same App Group on both targets, such as group.com.example.app.notirabbit.
  4. Add these entries to both Info.plist files, using your public key and shared App Group:
Info.plist entries · app and extension
<key>NotirabbitAppKey</key>
<string>YOUR_PUBLIC_APP_KEY</string>
<key>NotirabbitAppGroup</key>
<string>group.com.example.app.notirabbit</string>

The SDK includes the API and event URLs; no endpoint environment variables are required. APNs keys and the Sending API secret stay in the dashboard or on your server.

Replace the generated extension implementation with:

NotificationService.swift
import NotirabbitNotificationService

final class NotificationService: NotirabbitNotificationService {}

Keep the extension’s NSExtensionPointIdentifier as com.apple.usernotifications.service and NSExtensionPrincipalClass as $(PRODUCT_MODULE_NAME).NotificationService. The extension shares configuration through its own Info.plist and reports receipts through the App Group.

3. Connect the app lifecycle

Retain one client for the app’s lifetime. Create it on the main actor in didFinishLaunchingWithOptions and install your notification-center delegate before launch returns.

Client setup · inside your app delegate
let push = try Notirabbit(configuration: Configuration.from())
push.onError = { error in
    // Report the failure without logging tokens or notification payloads.
}
push.onClick = { notification in
    // Validate notification.destination, then pass it to your app's router.
}
try await push.start() // Does not show the permission prompt.

Run the asynchronous start() call in a task and handle errors. The SDK uses explicit callbacks; it does not replace your app delegate or open notification URLs for you.

iOS lifecycle integration
App callbackForward to the SDK
APNs device tokentry await push.didRegisterForRemoteNotifications(deviceToken: deviceToken)
Foreground notificationtry push.receive(userInfo: notification.request.content.userInfo)
Default notification taptry push.click(userInfo: response.notification.request.content.userInfo)

Always call the OS completion handler, including on errors. Ignore dismissal actions, choose foreground presentation in your app and retain other providers’ handlers if you use them.

Complete SwiftUI / AppDelegate example
NotirabbitDemo.swift
import SwiftUI
import UserNotifications
import Notirabbit

@main
struct NotirabbitDemo: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    var body: some Scene {
        WindowGroup {
            VStack(spacing: 20) {
                Text("Notirabbit sample")
                Button("Enable notifications") {
                    Task {
                        do { try await appDelegate.push?.requestPermission() }
                        catch { appDelegate.handle(error) }
                    }
                }
                Button("Unsubscribe") {
                    Task {
                        do { try await appDelegate.push?.unsubscribe() }
                        catch { appDelegate.handle(error) }
                    }
                }
            }
        }
    }
}

@MainActor
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    var push: Notirabbit?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        // Install the delegate before returning, including a notification cold launch.
        UNUserNotificationCenter.current().delegate = self
        do {
            let client = try Notirabbit(configuration: Configuration.from())
            push = client
            client.onError = { [weak self] error in self?.handle(error) }
            client.onClick = { notification in
                // Replace with your app's router. Validate the host/scheme before opening any URL.
                guard let url = URL(string: notification.destination),
                      url.scheme == "https", url.host == "example.com" else { return }
                UIApplication.shared.open(url)
            }
            Task {
                do { try await client.start() }
                catch { handle(error) }
            }
        } catch { handle(error) }
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Task {
            do { try await push?.didRegisterForRemoteNotifications(deviceToken: deviceToken) }
            catch { handle(error) }
        }
    }
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { handle(error) }

    nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification,
                                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        Task { @MainActor in
            guard (try? PushNotification(userInfo: notification.request.content.userInfo)) != nil else {
                completionHandler([]); return
            }
            do { try push?.receive(userInfo: notification.request.content.userInfo) } catch { handle(error) }
            completionHandler([.banner, .list, .sound])
        }
    }
    nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
                                           withCompletionHandler completionHandler: @escaping () -> Void) {
        Task { @MainActor in
            defer { completionHandler() }
            guard response.actionIdentifier == UNNotificationDefaultActionIdentifier else { return }
            do { try push?.click(userInfo: response.notification.request.content.userInfo) } catch { handle(error) }
        }
    }
    func handle(_ error: Error) {
        // Replace with application diagnostics/UI. Never log device tokens, secrets or payloads.
        NSLog("Notirabbit operation failed: %@", String(describing: type(of: error)))
    }
}

The complete example handles foreground, background and cold-start taps. Replace its destination allowlist and navigation with your own. UIKit applications use the same AppDelegate callbacks.

Custom links must be registered by your app and allowed under App Settings → General → Mobile deep-link schemes. See mobile destinations.

4. Ask at the right moment

Call requestPermission() from your opt-in action. The SDK handles registration after permission is granted and the OS returns an APNs token.

Call from your opt-in action
try await push.requestPermission()

Call try await push.unsubscribe() from a separate opt-out action. An explicit unsubscribe survives token refresh and relaunch until the user opts in again. getState() reads local state, not a fresh server query. Use retryPending() to retry retained work after connectivity returns.

5. Test on a signed iPhone

Use a physical device for APNs registration; simulator registration is not supported by this SDK. The APNs environment follows the signed provisioning profile, with production used by profile-free App Store/TestFlight builds.

  1. Install your signed build, grant permission and find the registered device in the dashboard.
  2. Send a selected-device test to your own device.
  3. Check foreground, background and cold-start taps, rich images and receipt/click reporting. Repeat with a TestFlight build before shipping.
  • No device registration: check Push Notifications signing, the public key, bundle ID and permission.
  • No image or receipt: check that the extension is embedded and signed, and both targets have matching App Group and Info.plist values.
  • A tap does not navigate: check delegate forwarding and your destination allowlist. Analytics and navigation are independent.
Send to iOS with the API

Source and release status

Source: notirabbit/notirabbit-ios. The current 0.1.0 source is available through branch or revision installation; a stable version tag has not been published.

Automated tests and unsigned builds have passed. Signed-device and TestFlight acceptance remain separate; the repository’s validation record tracks them.

The native SDK uses its own storage. Moving an existing Expo integration to Swift requires a separate identity and pending-event migration.