SDKs
Swift

Notifiable registers an iOS app with Notifiable: connect with a public key, ask for notification permission, identify the user, list their inbox, record opens and taps, and track events.
Import Notifiable only. Requires iOS 16. The SDK owns APNs — you do not pass a push port.
Install
In Xcode, add https://github.com/itsnotifiable/swift and the Notifiable product to the app target.
dependencies: [
.package(url: "https://github.com/itsnotifiable/swift", from: "0.1.1"),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "Notifiable", package: "swift"),
]
),
]
Attach NotifiableAppDelegate so the SDK receives the APNs device token (no UIApplication swizzling):
import SwiftUI
import Notifiable
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(NotifiableAppDelegate.self) private var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
If the app already has a delegate, forward the token instead:
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
NotifiableClient.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
NotifiableClient.didFailToRegisterForRemoteNotifications()
}
Integrate
Add .notifiable on the root view. It builds the client, puts it in the environment, and calls start() when the view appears.
ContentView()
.notifiable(
publicKey: "ntf_pub_...",
deepLinks: [
DeepLink(path: "/orders", label: "Orders"),
DeepLink(path: "/inbox", label: "Inbox"),
],
onDeepLink: { url in
// Open this route with your navigator.
}
)
| Argument | Required | Description |
|---|---|---|
publicKey |
yes | Key from the Notifiable dashboard. Development key while building; production key for the store build. |
deepLinks |
no | Routes Notifiable can link to: DeepLink(path:label:). |
onDeepLink |
no | Called with a notification url (e.g. development toast tap). |
logger |
no | debug / info / warn / error callbacks. |
Read the client from any child:
struct Home: View {
@EnvironmentObject private var notifiable: NotifiableClient
var body: some View {
Text(notifiable.snapshot.ready ? "Connected" : "Connecting…")
}
}
NotifiableClient is on the main actor — call methods from Task { } on the main actor.
Or build the client yourself (attach the app delegate first):
let notifiable = try NotifiableClient(
publicKey: "ntf_pub_...",
deepLinks: [DeepLink(path: "/orders", label: "Orders")],
onDeepLink: { url in /* navigate */ }
)
try await notifiable.start()
try await notifiable.requestPermission()
try await notifiable.identify(email: "[email protected]", externalId: "user_123")
Session storage defaults to UserDefaults. Pass storage (getItem / setItem / removeItem) to override.
Permission
Button("Enable notifications") {
Task {
try await notifiable.requestPermission()
}
}
Or ask from the Notifiable dashboard — the next connect opens the system prompt. provider is apns. A granted permission is saved with the hex device token. Connect before allow still saves not_determined with no token.
NotifiableClient API
Wait until snapshot.ready is true before identify, inbox, notification, openNotification, clickDelivery, or track.
State (snapshot)
| Field | Meaning |
|---|---|
ready |
Connect finished and the subscriber was saved. |
permission |
.granted / .denied / .notDetermined |
subscriber |
Device/person record, or nil until connect. |
clientConfig |
Dashboard app record (ios.bundle_id, environment). |
deepLinks |
Saved routes, or nil. |
devIndicator / diagnostics |
Development mark and crash-sharing choices. |
error |
Last NotifiableError, or nil. |
configuration holds the options this client was created with.
Methods
start() — Connects. .notifiable calls this on appear.
requestPermission() — System prompt, registers for APNs, saves. Returns the saved permission.
identify(email:externalId:firstName:lastName:) — Attach a signed-in user. At least one field required; blank email/external id rejected.
try await notifiable.identify(
email: "[email protected]",
externalId: "user_123",
firstName: "Ada",
lastName: "Lovelace"
)
inbox(query:) — One page of notifications (normally 20).
let page = try await notifiable.inbox()
let unread = try await notifiable.inbox(query: InboxQuery(page: 1, opened: false))
Each SubscriberNotification includes id, title, body, url, imageUrl, data, opened, openedAt, createdAt, deliveryId, and related fields.
notification(_:) / openNotification(_:) / clickDelivery(_:) — Load, mark opened, record tap.
track(name:properties:) — Event for the connected subscriber.
try await notifiable.track(
"order.shipped",
properties: [
"order": .string("1001"),
"express": .bool(true),
]
)
setDevIndicator(enabled:) / setDiagnostics(enabled:) — Development mark and diagnostics. No-ops for a production key.
Development vs production
| Development key | Production key | |
|---|---|---|
| First launch | Sheet to show/hide the Notifiable mark | No sheet |
| On screen | Draggable mark (bundle id check) | Nothing overlaid |
| Notifications | Recent unread can appear as top toasts while active + granted | System displays APNs |
| Diagnostics | Failed SDK calls can be shared | Nothing sent |
On the simulator, ⌘N shows the mark again. Preview toasts wait until permission is granted (needs a token).
Errors
Failed calls throw NotifiableError. snapshot.error.code matches the last failure from start, requestPermission, and identify.
| Code | Meaning |
|---|---|
invalid_config |
Blank public key, identify without a field, or permission without Apple push registered. |
invalid_public_key |
Key rejected. |
network |
Request failed or unexpected JSON. |
validation_failed |
Rejected value. |
subscriber_not_found |
Connect not finished, or subscriber gone. |
subscriber_limit |
Project cannot add another subscriber. |
rate_limited |
Too many requests. |
missing_token |
Granted permission but no device token (default port saves not_determined). |
missing_storage |
Reserved (default is UserDefaults). |
missing_provider |
Reserved. |
not_implemented |
Reserved. |
Tests
cd swift && swift test
For AI agents
Agents should read this page as Markdown: /swift.md. Start from /llms.txt or /ai-agents.md when the framework is unknown.
Rules
- Import
Notifiableonly. Do not import internal targets from app code. - Attach
NotifiableAppDelegate(or forward the APNs token) before requesting permission. - Add
.notifiable(publicKey:deepLinks:onDeepLink:)on the root view, or constructNotifiableClientand callstart(). - Wait for
snapshot.readybefore identify / inbox / track APIs. CallrequestPermission()from a button insideTask { }. - After sign-in, call
identifywithemailand/orexternalId. - Wire
onDeepLinkto the app’s navigation. RecordclickDeliverywhen handling taps from a custom inbox. - Do not invent hosts, env names, or CORS settings. Development key for local builds; production key for App Store builds.
Integration prompt
Copy this into a coding agent when wiring the SDK into an existing iOS / SwiftUI app:
Integrate the Notifiable Swift package into this iOS project.
1. Add the Notifiable SPM product (https://github.com/itsnotifiable/swift) to the app target.
2. Attach NotifiableAppDelegate via @UIApplicationDelegateAdaptor, or forward didRegisterForRemoteNotifications / didFailToRegister to NotifiableClient if an app delegate already exists.
3. On the root SwiftUI view, add .notifiable(publicKey:deepLinks:onDeepLink:) with the project’s public key (development for debug, production for release).
4. Implement onDeepLink to open the notification path with this app’s NavigationStack / router.
5. After sign-in, when snapshot.ready is true, call identify(email:externalId:).
6. Add an “Enable notifications” button that calls requestPermission() inside Task { }.
7. If there is an inbox screen, use inbox(), openNotification(_:), and clickDelivery(_:).
8. Call NotifiableClient APIs on the main actor. Do not explain CORS, API hosts, or environments — only the public key. Keep changes minimal and match existing project patterns.
