Swift (iOS)

Docs

Swift iOS SDK

Add Rejourney to native iOS apps with the Swift SDK for session replay, privacy-safe telemetry, crashes, and mobile product analytics.

Use AI to integrate faster

Using Cursor, Claude, or ChatGPT? Copy the integration prompt and paste it into your AI assistant to auto-generate the setup code.

Installation

Swift Package Manager

Add the Rejourney package in Xcode via File → Add Package Dependencies and enter:

https://github.com/rejourneyco/rejourney

Or add it directly to your Package.swift:

dependencies: [
.package(url: "https://github.com/rejourneyco/rejourney", from: "0.2.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "Rejourney", package: "rejourney")
]
)
]

[!NOTE] Rejourney requires iOS 15.1 or later.

Swift Setup

Initialize and start Rejourney in your @main App struct.

import SwiftUI
import Rejourney
@main
struct MyApp: App {
@MainActor
init() {
Rejourney.configure(publicKey: "rj_your_public_key")
Task { await Rejourney.start() }
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

If you use UIApplicationDelegate, call configure in application(_:didFinishLaunchingWithOptions:):

import UIKit
import Rejourney
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
@MainActor
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
Rejourney.configure(publicKey: "rj_your_public_key")
Task { await Rejourney.start() }
return true
}
}

Recording starts as soon as start() resolves. You can check the result if needed:

let result = await Rejourney.start()
if result.success, let sessionId = result.sessionId {
print("Recording started — session: \(sessionId)")
}

Remote Recording Settings

Project Settings can control Swift recording defaults without shipping a new app build. Supported SDK versions read these settings when start() is called:

SettingBehavior
Sample rateDefaults to 100%. Sampled-in sessions capture normally. Sampled-out sessions return before replay capture, network interception, uploads, or other package work starts.
Max observability durationLimits the maximum length of each observability session.
Recording FPSDefaults to 1 FPS. Project admins can choose 1, 2, or 3 FPS. If remote config is unavailable, the SDK falls back to local/default capture behavior.
Text input privacyDefaults to masking all text inputs. Secure-only mode keeps password/secure fields masked and allows other text inputs to appear in debugging replays.

Screen Tracking

Rejourney does not hook into SwiftUI navigation automatically, so call trackScreen whenever the user navigates to a new screen.

SwiftUI

Use .onAppear or a navigation-aware modifier:

struct CountriesListView: View {
var body: some View {
List { /* ... */ }
.onAppear {
Rejourney.trackScreen("Countries List")
}
}
}

UIKit

Call trackScreen inside viewDidAppear:

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Rejourney.trackScreen("Checkout")
}

Observe the navigation path and track on change:

@State private var path = NavigationPath()
NavigationStack(path: $path) {
ContentView()
}
.onChange(of: path) { _ in
// derive screen name from path and call trackScreen
Rejourney.trackScreen(currentScreenName(from: path))
}

User Identification

Associate sessions with your own user IDs so you can find specific users in the dashboard.

import Rejourney
// After login
Rejourney.identify("user_abc123")
// On logout
Rejourney.clearIdentity()

[!IMPORTANT] Privacy: Use internal IDs or UUIDs. If you must use PII (email, phone), hash it before passing it in.

Identity is persisted across app launches via UserDefaults — you only need to call identify once per login, not on every app open.

Custom Events

Track meaningful user actions to understand behaviour, debug issues, and filter session replays in the dashboard.

Basic Usage

import Rejourney
// Simple event (name only)
Rejourney.logEvent("signup_completed")
// Event with properties
Rejourney.logEvent("button_tapped", properties: ["buttonName": "get_started"])

API

Rejourney.logEvent(_ name: String, properties: [String: RejourneyMetadataValue] = [:])
ParameterTypeRequiredDescription
nameStringYesEvent name — use snake_case for consistency
properties[String: RejourneyMetadataValue]NoKey-value pairs attached to this event

RejourneyMetadataValue accepts Swift literals directly — no wrapping needed:

Rejourney.logEvent("purchase_completed", properties: [
"plan": "pro", // String literal
"amount": 29.99, // Double literal
"quantity": 1, // Int literal
"trial": false // Bool literal
])

Examples

// E-commerce
Rejourney.logEvent("purchase_completed", properties: [
"plan": "pro",
"amount": 29.99,
"currency": "USD"
])
// Onboarding
Rejourney.logEvent("onboarding_step", properties: [
"step": 3,
"stepName": "profile_setup",
"skipped": false
])
// Feature usage
Rejourney.logEvent("feature_used", properties: [
"feature": "dark_mode",
"enabled": true
])
// Errors / edge cases
Rejourney.logEvent("payment_failed", properties: [
"errorCode": "card_declined",
"retryCount": 2
])

How Events Appear in the Dashboard

Custom events are stored per-session and visible in two places:

  1. Session Replay Timeline — Events appear as markers on the replay timeline so you can jump to the exact moment an action occurred.
  2. Session Archive Filters — Filter the session list by:
    • Event name — Find all sessions containing a specific event (e.g. purchase_completed)
    • Event count — Find sessions with a specific number of custom events

Best Practices

[!TIP]

  • Use consistent naming (snake_case, e.g. button_tapped not Button Tapped)
  • Keep property values simple (strings, numbers, booleans) — avoid deeply nested objects
  • Focus on actions that matter for debugging or analytics — don't log everything

Privacy Controls

Text inputs and camera views are automatically masked by default. Project admins can change the default text input masking level in Project Settings for supported SDK versions. Secure/password fields, camera views, and explicit masks remain protected.

To hide additional sensitive views, use the mask and unmask APIs:

import UIKit
import Rejourney
// Mask a view — appears as a solid rectangle in replays
Rejourney.mask(balanceLabel)
// Remove masking if needed
Rejourney.unmask(balanceLabel)

For SwiftUI, get the underlying UIView via a UIViewRepresentable wrapper or introspect.

Native sheets

Native sheet capture is enabled by default (captureNativeSheets: true). This allows app-owned native sheets and dialogs, such as payment authorization modals, to appear in debugging replays when the OS permits capture. Keyboard/text-input system sheets are excluded when text inputs are masked by default. When text input masking is set to secure fields only, keyboards are best-effort only and cannot be reliably captured because iOS may render them as protected or remote system surfaces. OS share sheets are also best-effort only and cannot be reliably captured when the system renders them as protected or remote surfaces.

Disable native sheet capture if you want visual replay to stay limited to the main app window:

Rejourney.configure(
publicKey: "rj_your_public_key",
options: RejourneyOptions(captureNativeSheets: false)
)

[!IMPORTANT] You are the Data Controller. Rejourney acts as a Data Processor on your behalf. You are responsible for ensuring your end-users are informed about session recording and that you have a valid legal basis for processing their data (e.g. consent or legitimate interests).

What you must do

  1. Disclose session recording in your app's privacy policy. Include language such as:

    "We use Rejourney to record anonymized AND non-anonymized session replays of your in-app activity to help us improve the product, track crashes and issues, and reduce product friction. Session data may include screen interactions, device information, and approximate location. Text inputs and sensitive UI elements are automatically masked and never captured."

  2. Gate recording behind consent (recommended for EEA users):

    // Configure early — before consent is known
    Rejourney.configure(publicKey: "rj_your_public_key")
    // Call start() only after the user accepts your privacy policy
    func onUserConsented() {
    Task { @MainActor in
    await Rejourney.start()
    }
    }
  3. Respect opt-outs. If a user withdraws consent, stop recording and clear their identity:

    func onUserOptedOut() {
    Task { @MainActor in
    await Rejourney.stop()
    Rejourney.clearIdentity()
    }
    }

Observe-Only Mode (No Visual Recording)

To capture errors, crashes, ANRs, and network activity without recording visual replays, set observeOnly: true:

Rejourney.configure(
publicKey: "rj_your_public_key",
options: RejourneyOptions(observeOnly: true)
)

When enabled, all telemetry is collected but no screenshots are taken — sessions will NOT appear in your Replays page but full analytics, error, network, and crash data is still captured. Useful when users have opted out of screen recording but you still want error visibility.

Note: This can be set conditionally per user based on a stored preference or consent flag:

let optedOut = UserDefaults.standard.bool(forKey: "noRecording")
Rejourney.configure(
publicKey: "rj_your_public_key",
options: RejourneyOptions(observeOnly: optedOut)
)

Network capture

Network request capture (autoTrackNetwork: true by default) intercepts URLSession traffic via a custom URLProtocol. Disable it if you do not want network data collected:

Rejourney.configure(
publicKey: "rj_your_public_key",
options: RejourneyOptions(autoTrackNetwork: false)
)

Geolocation

IP-derived geolocation (country, region, city) is collected by default. Disable it to suppress the lookup entirely:

Rejourney.configure(
publicKey: "rj_your_public_key",
options: RejourneyOptions(collectGeoLocation: false)
)

Configuration Reference

All options are set once in configure and cannot be changed after start is called.

Rejourney.configure(
publicKey: "rj_your_public_key",
options: RejourneyOptions(
apiURL: URL(string: "https://api.rejourney.co")!,
userId: nil,
enabled: true,
observeOnly: false,
captureFPS: nil,
captureQuality: .medium,
wifiOnly: false,
captureScreen: true,
captureAnalytics: true,
captureCrashes: true,
captureANR: true,
trackConsoleLogs: true,
collectGeoLocation: true,
autoTrackNetwork: true,
captureNativeSheets: true,
debug: false
)
)
OptionTypeDefaultDescription
apiURLURLhttps://api.rejourney.coOverride for self-hosted deployments
userIdString?nilOptional initial internal user ID
enabledBooltrueMaster kill switch — set to false to disable the SDK entirely
observeOnlyBoolfalseCollect telemetry only, no visual recording
captureFPSInt?nilOptional local capture FPS fallback. Remote Project Settings recording FPS takes precedence when available
captureQualityRejourneyCaptureQuality.mediumJPEG capture quality (.low, .medium, .high)
wifiOnlyBoolfalseOnly upload session data on Wi-Fi
captureScreenBooltrueEnable/disable visual screen capture
captureAnalyticsBooltrueEnable/disable analytics event collection
captureCrashesBooltrueEnable/disable crash reporting
captureANRBooltrueEnable/disable ANR (App Not Responding) detection
trackConsoleLogsBooltrueCapture console logs for the session
collectGeoLocationBooltrueCollect IP-derived geolocation
autoTrackNetworkBooltrueIntercept URLSession requests for network capture
captureNativeSheetsBooltrueInclude app-owned native sheet/dialog windows in visual replay when iOS permits capture. OS share sheets and keyboards may be protected or remote surfaces and cannot be reliably captured
debugBoolfalsePrint verbose SDK logs to the console

Stopping Recording

Stop the current session and flush pending data:

let result = await Rejourney.stop()
print("Session \(result.sessionId ?? "unknown") ended — uploaded: \(result.uploadSuccess)")

The callback variant is available for non-async contexts:

Rejourney.stop { result in
print("Stopped: \(result.success)")
}

Session ID

Access the current session ID at any time to correlate with your own logs or support tooling:

if let sessionId = Rejourney.currentSessionId {
print("Rejourney session: \(sessionId)")
}