---
title: Swift (iOS)
description: "This article explains how to perform a full integration of an iOS app with *Releva*, using the Releva Swift SDK. ### Requirements - iOS 15.0 or later - Swift 5.7 language mode or later - Firebase, for push notifications. The…"
url: "https://releva.ai/documentation/integrations/swift-ios/"
updated: "2026-09-22"
category: Integrations
---

# Swift (iOS)

This article explains how to perform a full integration of an iOS app with *Releva*, using the Releva Swift SDK.

### Requirements

- iOS 15.0 or later
- Swift 5.7 language mode or later
- Firebase, for push notifications. The SDK accepts `firebase-ios-sdk` 11.15.0 up to (but not including) 13.0.0, so both Firebase 11 and 12 resolve. Your Xcode version has to satisfy whichever one you land on — Firebase raises its toolchain floor over time, including within a major, so pinning to `"11.15.0"..<"12.0.0"` is a supported way to stay on an older Xcode.

### Install the SDK

Swift Package Manager is the only supported distribution channel. The package exposes two products: `RelevaSDK` for your app target, and `RelevaNotificationExtension` for a Notification Service Extension if you want rich push notifications.

Your app target also needs `FirebaseMessaging` and `FirebaseCore` declared **explicitly**. Firebase is resolved as a dependency of the SDK, but Swift Package Manager only lets a target use products from packages your own manifest declares — and the push setup below calls `Messaging.messaging()` from your code.

#### In a Package.swift manifest

```
dependencies: [
    .package(url: "https://github.com/Releva-ai/sdk-swift.git", from: "5.0.0"),
    .package(url: "https://github.com/firebase/firebase-ios-sdk.git", "11.15.0"..<"13.0.0")
]
```

```
.target(
    name: "YourApp",
    dependencies: [
        .product(name: "RelevaSDK", package: "sdk-swift"),
        .product(name: "FirebaseMessaging", package: "firebase-ios-sdk"),
        .product(name: "FirebaseCore", package: "firebase-ios-sdk")
    ]
)
```

#### In Xcode

1. File → Add Package Dependencies…
2. Enter `https://github.com/Releva-ai/sdk-swift.git`, with the dependency rule “Up to Next Major Version” from `5.0.0`.
3. Add the `RelevaSDK` product to your app target.
4. Add `https://github.com/firebase/firebase-ios-sdk.git` as a second package and add its `FirebaseMessaging` and `FirebaseCore` products to your app target.
5. For rich push notifications, add the `RelevaNotificationExtension` product to your Notification Service Extension target.

The SDK does not set Firebase up for you. You still need a `GoogleService-Info.plist` in your app target and a `FirebaseApp.configure()` call during launch, from the standard Firebase iOS setup.

### Initialize the Releva Client

Create the client during app launch and set the device and profile identifiers straight away.

```
import RelevaSDK

// realm - use "" unless instructed otherwise by your account manager
// accessToken - from Releva's admin panel, under Settings
let client = RelevaClient(
    realm: "",
    accessToken: "<yourAccessToken>",
    config: RelevaConfig.full()
)

client.setDeviceId(UIDevice.current.identifierForVendor?.uuidString ?? "")

// The id you use internally to identify this user
client.setProfileId("<profileId>")
```

`RelevaClient` is `@MainActor`-isolated. Methods that give you a result or an error back are `async throws`; the rest are fire-and-forget, and several of those still reach the network from a task the SDK starts itself.

### Identify the visitor

The SDK identifies a user solely by the `profileId` you set. Contact details and other profile attributes are never sent from the app — send those from your backend through the [Create / Update Profile API](https://releva.ai/documentation/profiles-2/profile-upsert/).

On **login**, set the profile id normally and the anonymous profile the user browsed under is merged into it, so their pre-login behaviour is kept:

```
client.setProfileId(loggedInUserId)
```

On **logout**, generate a new anonymous id and pass `true` as the second argument to skip that merge — otherwise the next person using the device inherits the previous user's profile. Because the merge is skipped, the push token has to be registered against the new profile explicitly:

```
let newAnonymousProfileId = UUID().uuidString
client.setProfileId(newAnonymousProfileId, true)

Task {
    do {
        let fcmToken = try await Messaging.messaging().token()
        try await client.registerPushToken(fcmToken, deviceType: .ios)
    } catch {
        print("Failed to re-register push token: (error)")
    }
}
```

Keeping one client and calling `setProfileId` on it is the simplest approach. If your app builds a *new* client on login or logout instead, shut the old one down first — otherwise it stays alive and keeps re-registering the push token under the previous profile on every foreground:

```
oldClient.shutdown()
let client = RelevaClient(realm: realm, accessToken: accessToken, config: config)
```

### Push notifications

#### Enable the capabilities

1. In Xcode, select your app target → Signing & Capabilities.
2. Add “Push Notifications”.
3. Add “Background Modes” and tick “Remote notifications”.

#### Register for notifications

Request permission, then register the token and enable engagement tracking. Set `pushTokenProvider` as well: FCM rotates tokens silently, and that closure is what lets the SDK re-fetch and re-upload the current token on every launch and foreground. Without it the stored token drifts stale and pushes start failing with “device token expired”.

```
import UserNotifications
import FirebaseMessaging

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
    if granted {
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}

// In AppDelegate
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    // Only needed if your Firebase project has no APNs Authentication Key configured:
    // Messaging.messaging().apnsToken = deviceToken

    client.pushTokenProvider = { completion in
        Messaging.messaging().token { token, _ in
            completion(token)
        }
    }

    Task {
        do {
            let fcmToken = try await Messaging.messaging().token()
            try await client.registerPushToken(fcmToken, deviceType: .ios)
            client.enablePushEngagementTracking()
        } catch {
            print("Failed to register push token: (error)")
        }
    }
}

// Re-register whenever FCM rotates the token at runtime.
// Assign Messaging.messaging().delegate = self during launch, or this never fires.
extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        guard let fcmToken = fcmToken else { return }
        Task { try? await client.registerPushToken(fcmToken, deviceType: .ios) }
    }
}

// Track taps
func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    let userInfo = response.notification.request.content.userInfo

    if client.isRelevaMessage(userInfo: userInfo) {
        client.trackEngagement(userInfo: userInfo, type: .opened)
    }

    completionHandler()
}
```

#### Rich notifications

For images and custom action buttons, add a Notification Service Extension (File → New → Target → Notification Service Extension), add the `RelevaNotificationExtension` product to it, and replace the generated `NotificationService.swift` with:

```
import UserNotifications
import RelevaNotificationExtension

class NotificationService: RelevaNotificationServiceExtension {
    // That is all. The SDK handles the rich notification processing.
}
```

#### Navigating from a notification

When a user taps a notification, the SDK posts a `NotificationCenter` notification and your app performs the navigation. Observe the ones you support:

```
NotificationCenter.default.addObserver(
    forName: Notification.Name("RelevaNavigateToScreen"), object: nil, queue: .main
) { notification in
    guard let screen = notification.userInfo?["screen"] as? String else { return }
    let parameters = notification.userInfo?["parsedParameters"] as? [String: Any]

    // `screen` is the free-form value configured in the Releva dashboard
    switch screen {
    case "cart":
        break // navigate to the cart
    case "product_details":
        let productId = parameters?["productId"] as? String
        break // navigate to that product
    default:
        break // home, or handle unknown screens
    }
}

NotificationCenter.default.addObserver(
    forName: Notification.Name("RelevaNavigateToURL"), object: nil, queue: .main
) { notification in
    guard let url = notification.userInfo?["url"] as? URL else { return }
    // A deep link with your own scheme, e.g. myapp://product/123
}

NotificationCenter.default.addObserver(
    forName: Notification.Name("RelevaNavigateToInbox"), object: nil, queue: .main
) { notification in
    let parameters = notification.userInfo?["parsedParameters"] as? [String: Any]
    let inboxMessageId = parameters?["inboxMessageId"]
    // Navigate to the inbox, optionally opening that message
}
```

External `https://` links are opened in Safari by the SDK directly — only your own custom schemes post `RelevaNavigateToURL`.

### Track what the visitor is doing

Screen views, product views, search, checkout and recommendations all go through one call: build a `PushRequest` with the fluent builder, then hand it to `client.push(...)`.

**Every screen that should record a page view, or receive banners and stories, needs its own page token** from the Pages section of the Releva admin. A screen view sent without a token, or with one the backend does not know, is accepted but records no page view and returns no banners or stories — nothing is rejected, so a missing token is easy to miss.

```
// Screen view
let request = PushRequest()
    .screenView("<yourPageToken>")
    .pageProductIds(["product-1", "product-2"])
    .pageCategories(["electronics", "phones"])
    .locale("en_US")
    .currency("USD")

do {
    let response = try await client.push(request)
    for recommender in response.recommenders {
        print("Recommender: (recommender.name)")
        for product in recommender.response {
            print("- (product.name): (product.price)")
        }
    }
} catch {
    print("Error: (error)")
}

// Product view
let product = ViewedProduct(id: "product-123")
    .withStringField(key: "brand", values: ["Apple"])

try await client.push(
    PushRequest().screenView("<productPageToken>").productView(product)
)

// Search
try await client.push(
    PushRequest()
        .screenView("<searchPageToken>")
        .search("iPhone")
        .pageProductIds(["product-1", "product-2", "product-3"])
)
```

`PushRequest` is a value type: every builder returns a new copy rather than changing the one you called it on, so a part-built request can be safely reused as the base for several pushes. That also means calling a builder as a bare statement throws the edit away — the compiler warns about it.

```
let base = PushRequest().locale("en_US").currency("USD")

let home = base.screenView("<homeToken>")            // base is unchanged
let search = base.screenView("<searchToken>").search("iPhone")
```

The SDK sends no page URL, so URL-based targeting rules on banners and stories do not apply on iOS. Target by page token or by segment instead.

#### Cart and wishlist

Both are stateful: set them on the client and the SDK syncs the changes for you.

```
let product1 = CartProduct(id: "sku-123", price: 29.99, quantity: 2)
let product2 = CartProduct(id: "sku-456", price: 49.99, quantity: 1)

client.setCart(Cart.active([product1, product2]))

client.setWishlist([
    WishlistProduct(id: "product-1"),
    WishlistProduct(id: "product-2")
])

// Checkout. The user is identified by the profileId you already set.
let orderedCart = Cart.paid([product1, product2], orderId: "order-789")
try await client.trackCheckoutSuccess(orderedCart: orderedCart, screenToken: "<checkoutToken>")
```

#### Custom events

```
let customFields = CustomFields()
    .withStringField(key: "color", values: ["red"])
    .withNumericField(key: "discountPercent", values: [15])
    .withDateField(key: "promoEndsAt", values: [Date()])

let event = CustomEvent(action: "selectedColor")
    .withProduct(id: "product-123", quantity: 1)
    .withTag("promo")
    .withCustomFields(customFields)

try await client.trackCustomEvent(event)
```

Date fields take native `Date` values and are serialized to ISO-8601 for you.

#### Filtering

```
let complexFilter = NestedFilter.and(
    SimpleFilter.priceRange(minPrice: 100, maxPrice: 500),
    SimpleFilter.brand("Apple")
)

try await client.push(
    PushRequest().screenView("<categoryPageToken>").pageFilter(complexFilter)
)
```

### On-screen content

Banners, stories, NPS surveys and the App Inbox all arrive in the response to a push and are configured in the Releva dashboard. Add the ones you want.

```
HomeView()
    .bannerDisplay(client: client, targetSelector: "#home-content") { url in
        handleDeepLink(url)
    }
    .storyDisplay(client: client) { url in
        handleDeepLink(url)
    }

// NPS on the root view
ContentView()
    .npsDisplay(onSubmit: { token, score, comment in
        Task { try? await client.submitNpsResponse(token: token, score: score, comment: comment) }
    })

client.setAppVersion("1.2.3")   // lets the server target surveys by app version

// App Inbox, after the profile id is set
client.initializeInbox()
```

Two things to know about triggers:

- **Banners reset on every screen view.** Navigating back to a screen and tracking it again re-evaluates the triggers, so a banner can show again — the same behaviour as on the web.
- **Scroll triggers need your help.** The SDK cannot see your scroll view. In SwiftUI attach `.relevaScrollTracking(client)` to the *content* of the `ScrollView`, not to the `ScrollView` itself; from UIKit call `client.reportScrollPercentage(percent)` with a value from 0 to 100. Both require push notifications to be enabled in the config — that is what creates the banner and story managers — and are otherwise silent no-ops.

Leave-intent triggers are a web-only feature and never fire on mobile. Impressions, clicks and dismissals are tracked automatically.

### Configuration options

Start from a preset, or build your own:

```
let config = RelevaConfig.full()          // everything on (default)
let config = RelevaConfig.trackingOnly()  // no push notifications
let config = RelevaConfig.pushOnly()      // no tracking

let config = RelevaConfig(
    enableTracking: true,
    enableScreenTracking: true,
    enablePushNotifications: true,
    enableAnalytics: true,
    enableDebugLogging: true,
    requestTimeoutInterval: 30.0,
    maxRetryAttempts: 3,
    engagementBatchSize: 10,
    engagementBatchInterval: 30.0
)
```

### Backend calls you still need

The app covers what the user does. Your catalogue, your orders and your consent changes still have to reach Releva from your backend:

1. Call the [Product Create / Update API](https://releva.ai/documentation/catalog/product-update/) whenever your catalogue changes.
2. Implement the [Periodical Product Sync](https://releva.ai/documentation/catalog/periodical-product-sync/) as a fallback, so a missed update cannot leave Releva stale.
3. Call the [Cart Paid API](https://releva.ai/documentation/orders-refunds/cart-paid/) when a customer places an order, and the [Refund API](https://releva.ai/documentation/orders-refunds/order-refund/) when one is refunded.
4. Call the [Subscribe](https://releva.ai/documentation/consent-status/subscribe/) and [Unsubscribe](https://releva.ai/documentation/consent-status/unsubscribe/) APIs when a user changes their marketing preferences.
5. Call the [Profile Register API](https://releva.ai/documentation/profiles-2/profile-register/) when a new user registers.

### All set!

Your app is integrated. Explore the [API Specification](https://releva.ai/documentation/developer-documentation/server-side-integration/) for the server-side calls that let you send custom product and profile data, which can then be used in personalization.
