Pushlanedocs
Documentation menu

iOS Quickstart

Install the SDK, register a device, and send your first push.

#Prerequisites

  • Xcode 15 or later
  • A paid Apple Developer account (required for APNs)
  • A physical iOS 16+ device — the Simulator has no APNs and can never receive a real push
  • Your Pushlane Tenant ID, Ingest URL, and write-key — copy them from Settings → Install SDK in the dashboard

#1. Install the SDK (Swift Package Manager)

In Xcode, go to File → Add Package Dependencies…, paste the URL below, and set the dependency rule to Up to Next Major Version.

url
https://github.com/Big-Terence/pushlane-ios-sdk

When the package resolves, add the products to the right targets:

swift
.package(url: "https://github.com/Big-Terence/pushlane-ios-sdk", from: "0.1.0")
// Then add the products you need to each target:
//   App target    → PushlaneCore, PushlanePush, PushlaneInApp
//   NSE target    → PushlaneNotificationService
Note
The SDK is distributed as a Swift Package (iOS 16+). Add it once in Xcode via File → Add Package Dependencies…, then attach each product to the target that needs it.

#2. Enable Push Notifications in Xcode

This step is easy to miss and is the most common reason a device never produces a token.

  1. Select your app target in Xcode.
  2. Go to Signing & Capabilities.
  3. Click + Capability and search for Push Notifications.
  4. Double-click to add it.
Heads up
Without the Push Notifications capability, UIApplication.registerForRemoteNotifications() silently fails on a real device with no valid aps-environment entitlement. No entitlement = no device token = the Verify step never turns green, regardless of your APNs key.

Optional: if you plan to send silent pushes (content-available: 1), also add the Background Modes capability and tick Remote notifications.

#3. Configure the SDK

Call the four setup functions in didFinishLaunchingWithOptions — order matters. Identify before tracking so the first event is always attributed to a user.

swift
import PushlaneCore
import PushlanePush
import PushlaneInApp

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

    Pushlane.configure(
        apiBase: URL(string: "https://<your-ingest-url>")!,
        tenantId: "<your-tenant-id>",
        publishableKey: "lpk_live_…",          // from Settings → Install SDK
        appGroup: "group.com.yourapp.pushlane"      // shared with your NSE target
    )
    Pushlane.identify(currentUser.id)   // your stable external user id (no label — positional)
    PushlaneInApp.start()               // emits app_open on cold launch; tracks IAM sessions
    PushlanePush.register()             // sets UNDelegate and requests permission BEFORE return

    return true
}

// Forward the APNs device token — the environment is auto-detected (sandbox vs prod)
func application(
    _ app: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken token: Data
) {
    PushlanePush.didRegister(deviceToken: token)
}

func application(
    _ app: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: Error
) {
    PushlanePush.didFailToRegister(error: error)
}
Note

SwiftUI app? Keep an @UIApplicationDelegateAdaptor to receive the device-token callbacks, then call the same four functions in your App.init():

swift
import SwiftUI
import PushlaneCore
import PushlanePush
import PushlaneInApp

@main
struct MyApp: App {
    // Keep an AppDelegate adapter to receive the device-token callbacks.
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

    init() {
        Pushlane.configure(
            apiBase: URL(string: "https://<your-ingest-url>")!,
            tenantId: "<your-tenant-id>",
            publishableKey: "lpk_live_…",
            appGroup: "group.com.yourapp.pushlane"
        )
        Pushlane.identify(currentUser.id)
        PushlaneInApp.start()
        PushlanePush.register()
    }

    var body: some Scene { WindowGroup { ContentView() } }
}

// AppDelegate.swift — handles the token callbacks
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ app: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken token: Data
    ) {
        PushlanePush.didRegister(deviceToken: token)
    }
    func application(
        _ app: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        PushlanePush.didFailToRegister(error: error)
    }
}
Heads up
Pushlane.identify takes a positional argument — no label. Pushlane.identify(currentUser.id) compiles. Pushlane.identify(externalId: currentUser.id) does not.

#4. Add the Notification Service Extension

The NSE runs in a separate process and does two things: it downloads rich media (image_url / loop_media) and emits a received event the moment a push lands — so you get a true open-rate (opened ÷ received).

Create a new target: in Xcode, File → New → Target… → choose Notification Service Extension. Give it a suffixed bundle id (e.g.com.yourapp.ios.NotificationService).

swift
// NotificationService.swift — inside your NSE target
import PushlaneNotificationService

class NotificationService: PushlaneNotificationService {
    // Use the SAME App Group id you passed to Pushlane.configure(appGroup:)
    override var pushlaneAppGroup: String? { "group.com.yourapp.pushlane" }
}

Then wire the App Group so the NSE can read the ingest config:

  1. Add the App Groups capability to both the app target and the NSE target.
  2. Use the same group id (e.g. group.com.yourapp.pushlane) in both.
  3. Pass that id as appGroup: in Pushlane.configure (app) and as pushlaneAppGroup in the NSE subclass.
Note
Without an App Group the NSE still renders rich media — it just won't emit received. The received event is best-effort and never blocks the notification from showing.

#5. Track events

Push is only as good as the events that trigger it. The SDK auto-emits five events; track the rest with Pushlane.track. Use snake_case names and typed property values — the backend coerces against your event catalogue, it never infers types from the first value.

swift
// Properties are typed (String / Int / Double / Bool / array).
// The backend coerces against your event catalogue — never infers from the first value.
Pushlane.track("lesson_finished", ["lesson_id": "intro-1", "duration_sec": 42])
Pushlane.track("paywall_viewed", ["placement": "home", "paywall_id": "fall_sale"])
Pushlane.track("subscription_cancelled", ["product_id": "monthly_pro", "reason": "price"])

Auto-emitted events (no call needed):

EventSourceWhen
app_openPushlaneInApp.start()Cold launch — emitted once per process
session_startedPushlaneInApp (automatic)Foreground return after ≥30 s in background
openedPushlanePush (automatic)User tapped a Pushlane push notification
receivedPushlaneNotificationService (NSE)Push landed on device — even if never opened
push_registration_failedPushlanePush (automatic)APNs refused to register the device
Heads up
Pushlane.track drops the event and logs a warning if called before Pushlane.identify. Always identify the user before tracking. On logout, call Pushlane.reset():
swift
// On logout: detach this device from the user so stale tokens aren't stored.
Pushlane.reset()

#6. Verify the integration

Build and run your app on a physical device. In the Pushlane dashboard, go to Settings → Install SDK (or open the Quickstart) and watch the live verification panel — it polls your backend every few seconds and lights up as real events and device registrations arrive.

CheckWhat it means
SDK detectedAt least one event has reached the ingest worker
Events instrumentedDistinct event names from your app appear in the catalogue
Device connectedA device token was registered — you can receive a push
Note
All three checks use live backend data — a check turns green only when the data is actually in the store. No mocks.

#Next step

The SDK is wired. To send real pushes to a device you need to upload your Apple push key (.p8) to Pushlane — that is the only remaining step before a buzz on the phone.

APNs setup (.p8) →