iOS SDK (Swift)
Native Swift Package, iOS 15+, direct APNs — no Firebase. URLSession and UserDefaults only.
Before the app side works you need the APNs key uploaded: iOS platform setup.
1 Add the package
Xcode → File → Add Package Dependencies… → enter the repository URL and choose the OpenNotification product. Or, if you vendor the monorepo, add it by path (packages/sdk-ios).
// Package.swift
dependencies: [
.package(url: "https://github.com/Aproder/opennotification.git", from: "0.1.0")
],
targets: [
.target(name: "App", dependencies: [
.product(name: "OpenNotification", package: "opennotification")
])
]
2 Capabilities
Target → Signing & Capabilities → + Capability: Push Notifications, and Background Modes → Remote notifications.
3 AppDelegate
The SDK needs four things forwarded to it. In AppDelegate.swift:
import UIKit
import UserNotifications
import OpenNotification
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ app: UIApplication,
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
OpenNotification.shared.configure(endpoint: "https://push.example.com", appKey: "pk_live_…")
OpenNotification.shared.onOpen = { message in
Router.open(message.url, data: message.data) // deep link, actionId, data
}
OpenNotification.shared.onReceive = { message in
// push arrived while the app was in the foreground
}
// A press that launched the app (cold start) — replayed into onOpen.
OpenNotification.shared.handleLaunch(userInfo: options?[.remoteNotification] as? [AnyHashable: Any])
UNUserNotificationCenter.current().delegate = self
return true
}
// APNs handed us a token → the SDK registers the device
func application(_ app: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
OpenNotification.shared.didRegister(deviceToken: token)
}
// The user pressed the notification or one of its buttons
func userNotificationCenter(_ c: UNUserNotificationCenter,
didReceive r: UNNotificationResponse,
withCompletionHandler done: @escaping () -> Void) {
OpenNotification.shared.didReceive(response: r)
done()
}
// Show the banner while the app is in the foreground
func userNotificationCenter(_ c: UNUserNotificationCenter,
willPresent n: UNNotification,
withCompletionHandler done: @escaping (UNNotificationPresentationOptions) -> Void) {
OpenNotification.shared.willPresent(notification: n)
done([.banner, .sound, .badge])
}
}
SwiftUI apps: keep an AppDelegate via @UIApplicationDelegateAdaptor(AppDelegate.self).
didRegisterPermission is granted, Apple issues a token, and nothing reaches the server — the device never subscribes. This is the most common "it doesn't work" cause.
4 Ask for permission
From a screen that explains why — not on first launch:
OpenNotification.shared.requestPermission { granted in
// granted → the SDK calls registerForRemoteNotifications();
// the token arrives in didRegister and the device is subscribed.
}
On registration the SDK sends: platform, token, sdkVersion, OS version, device model, app version, the device's language, timezone and country.
5 Identity and tags
OpenNotification.shared.login("user_123") // after your own sign-in
OpenNotification.shared.setTags(["plan": "platinum", "streak": 42])
OpenNotification.shared.removeTags(["streak"])
OpenNotification.shared.setLanguage("tr")
OpenNotification.shared.logout() // on sign-out
OpenNotification.shared.unsubscribe() // user opted out in your settings screen
OpenNotification.shared.trackSession() // optional: sent on didBecomeActive already
Every call takes an optional completion: (Error?) -> Void. login before the token arrives is fine — the external id is sent with the registration. Semantics: Identity.
OpenNotification.shared.subscriptionId gives the stored id, if you need it for support.
Silent (background) pushes
A campaign with Silent push arrives as content-available with no alert. Enable Background Modes › Remote notifications and forward the AppDelegate callback:
func application(_ app: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler done: @escaping (UIBackgroundFetchResult) -> Void) {
OpenNotification.shared.didReceiveBackground(userInfo: userInfo) { _ in done(.newData) }
}
OpenNotification.shared.onReceive = { message in
if message.silent { Sync.run(message.data) }
}
didReceiveBackground reports delivered (the service extension does not run for a background push) and hands message.data to onReceive. iOS gives you about 30 seconds.
Sessions
configure() observes didBecomeActive and pings POST /v1/subscriptions/:id/session; the server counts a new session after 30 minutes of quiet. sessionCount and lastSessionAt then work as segment fields.
Notification Service Extension
Required for delivered events, images and action buttons. iOS gives you none of those without an extension.
1 Xcode → File → New → Target… → Notification Service Extension. Name it NotificationService. Match the deployment target to the app.
2 Replace the generated NotificationService.swift with packages/sdk-ios/Templates/NotificationService.swift.
3 In the extension target's Info.plist, add a String key OpenNotificationApiUrl with your API base URL (https://push.example.com). The extension is a separate process and cannot read the app's configuration.
4 Give the extension its own App ID and provisioning profile (com.acme.shop.NotificationService) — automatic signing does this.
What the template does, in ~30 seconds of allowed runtime:
| Step | Detail |
|---|---|
Report delivered | POST /v1/e/d/<msgId> with a 5 s timeout so the notification never waits on analytics. |
| Download the image | From on.img, attached as UNNotificationAttachment. |
| Register the buttons | Reads on.a, registers a UNNotificationCategory named on_<campaignId> — iOS only shows buttons for a category it already knows, and the server sets aps.category to that name. |
The server always sends mutable-content: 1, which is what wakes the extension.
Handling opens
onOpen receives an OpenNotificationMessage:
public struct OpenNotificationMessage {
let msgId: String?, campaignId: String?
let title: String, body: String
let image: String?, url: String?
let actions: [NotificationAction] // {id, title, url?}
let actionId: String? // set when a button was pressed
let data: [String: String]
}
opened (body press) or clicked (button press) is reported automatically before your closure runs. iOS never reports dismissals.
Testing
swift testinpackages/sdk-iosruns on macOS; UIKit parts are compiled out with#if canImport.- On a device: the dashboard's Send a test on the schedule step, or
POST /v1/notificationswithtarget.externalIds. - The simulator can receive APNs pushes on Apple Silicon (Xcode 11.4+) via
xcrun simctl push, but the token it produces is not a real APNs token; use a device to test the full path.