Skip to content
Last9
Book demo

iOS / Swift

Install and configure the Last9 iOS RUM SDK. Swift API, CDN-hosted CocoaPods podspec and SPM XCFramework, automatic instrumentation for sessions, views, network, errors, and resource sampling.

Real User Monitoring for iOS apps. Automatic instrumentation for sessions, views, network requests, errors, and resource metrics via OpenTelemetry.

Prerequisites

  • iOS 15.1+
  • Swift 5.9+
  • Dependencies (resolved transitively): OpenTelemetry-Swift-Api ~> 1.10, OpenTelemetry-Swift-Sdk ~> 1.10
  • A Last9 RUM client token and OTLP endpoint

Create a Client Monitoring Token

  1. Open Last9 → Settings → Ingestion Tokens
  2. Click Create Token → choose type Client
  3. Set the allowed origin to ios://com.yourcompany.yourapp — use your app’s exact bundle ID
  4. Copy the token and the OTLP endpoint URL

CDN artifacts

ArtifactURL
Podspechttps://cdn.last9.io/rum-sdk/ios/builds/0.7.0/Last9RUM.podspec
XCFrameworkhttps://cdn.last9.io/rum-sdk/ios/builds/0.7.0/Last9RUM.xcframework.zip
Checksumhttps://cdn.last9.io/rum-sdk/ios/builds/0.7.0/Last9RUM.xcframework.zip.sha256

Staging builds use the -alpha suffix.

Installation

  1. Add to your Podfile

    pod 'Last9RUM', :podspec => 'https://cdn.last9.io/rum-sdk/ios/builds/0.7.0/Last9RUM.podspec'
  2. Install

    pod install

Initialization

import Last9RUM
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
var config = L9RumConfig(
baseUrl: "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",
clientToken: "your-client-token",
serviceName: "my-ios-app",
serviceVersion: "1.0.0",
deploymentEnvironment: "production"
)
// Use your app's bundle ID in ios:// format — must match the origin
// allowlist on your Last9 client token.
config.origin = "ios://com.example.myapp"
L9Rum.shared.initialize(config: config)
return true
}
}

What’s captured automatically

SignalDetails
Network requestsEvery URLSession call — latency, status code, URL
Screen views (UIKit)UIViewController lifecycle callbacks
Screen views (SwiftUI).trackView(name:) modifier
Sessions15m inactivity / 4h max, persisted across restarts
App launch timeCold and warm start duration
Resource metricsMemory and CPU sampled periodically
ErrorsUnhandled exceptions and crashes
ANR detectionMain thread blocks beyond threshold
view.ttfdTime from last HTTP response to next rendered frame — measures post-API render latency on data-driven screens

View time-to-full-display (view.ttfd)

view.ttfd measures how long it takes for the screen to render after the last API response completes. It captures the delta between the HTTP response timestamp and the next CADisplayLink callback — giving you end-to-end visibility on data-driven screens where content appears only after a network call.

AttributeTypeDescription
view.ttfdfloatMilliseconds between HTTP response end and next rendered frame
view.ttidfloatMilliseconds from screen open to first frame (unchanged)

How it works: after any L9URLProtocol response callback fires, the SDK schedules a CADisplayLink on the main run loop. On the next display frame, the delta is recorded as view.ttfd on the active View span.

  • Works on both SwiftUI and UIKit with no app code changes.
  • Applies to the View span that was active when the HTTP call was made.
  • If no View span is active when the response arrives, the measurement is skipped.

Configuration

var config = L9RumConfig(
// --- Required ---------------------------------------------------------
baseUrl: "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",
clientToken: "your-client-token",
serviceName: "my-ios-app",
serviceVersion: "1.0.0",
deploymentEnvironment: "production"
)
// --- Optional -------------------------------------------------------------
// Origin sent as X-LAST9-ORIGIN header.
// Required for client_monitoring tokens. Use ios://com.your.bundle.id —
// must match the origin allowlist configured on your Last9 client token.
config.origin = "ios://com.example.myapp"
// Specific build identifier (maps to app.build_id)
config.appBuildId = "1.0.0-build-42"
// Unique per-install ID (NOT a hardware ID). Persists across launches.
config.appInstallationId = nil
// Session sampling rate: 0-100 (percentage). 100 = sample everything.
config.sampleRate = 100
// Print debug logs to console
config.debugLogs = false
// Automatically trace HTTP requests via URLProtocol
config.networkInstrumentation = true
// Automatically capture unhandled exceptions
config.errorInstrumentation = true
// Max spans per export batch
config.maxExportBatchSize = 100
// Export timeout in milliseconds
config.exportTimeoutMs = 30_000
// Periodically sample memory and CPU
config.resourceMonitoringEnabled = true
// Interval between resource samples (ms)
config.resourceSamplingIntervalMs = 30_000
// Setting this to true will hide network requests (and their
// DNS/TCP/TLS/TTFB phase child spans) from the Last9 dashboard's
// Sessions → APIs tab. Each request would get its own traceId
// instead of sharing the current view's traceId, and that tab
// only fetches spans that share the View's traceId. Keep this
// false unless you specifically need per-request trace isolation.
config.isolateTracePerRequest = false
// Custom resource attributes added to every span
config.resourceAttributes = [
"app.platform": "ios",
]
// W3C Baggage propagation on outgoing requests
config.baggage = L9BaggageConfig()
config.baggage.enabled = false
config.baggage.allowedKeys = ["session.id", "user.id"]
config.baggage.maxTotalBytes = 8192
config.baggage.trackedUrlPatterns = []
config.baggage.warnAtPercentage = 80
// Substring patterns — matching URLs are skipped before span creation.
// Prefer ignorePatterns below for regex support and hostname/pathname targeting.
config.excludedUrlPatterns = [".jpg", ".png", ".pdf", "cdn.example.com"]
// Fine-grained network ignore rules. Matched URLs are dropped before span
// creation. .contains uses substring matching; .regex uses regex search semantics.
// Takes precedence over excludedUrlPatterns.
config.ignorePatterns = L9NetworkIgnorePatterns(
fullUrl: [
.contains("https://cdn.example.com"),
.regex("^https://.*\\.example\\.com", options: [.caseInsensitive]),
],
pathname: [
.contains(".pdf"),
.contains(".jpg"),
.regex("^/internal/metrics"),
],
hostname: [
.contains("cdn.example.com"),
.regex("(^|\\.)assets\\.example\\.com$", options: [.caseInsensitive]),
]
)
// Trace header propagation for ignored URLs.
// .preserve (default): keep traceparent on ignored requests.
// .strip: remove traceparent from ignored requests (e.g. third-party CDNs).
config.propagationMode = .preserve

Network phase child spans

When URLSession instrumentation is enabled, each parent HTTP span includes child spans for individual network phases:

Child spanWhat it measures
dnsDNS lookup duration
tcp_connectTCP connection establishment
tls_handshakeTLS negotiation
ttfbTime from request sent to first response byte

No SDK config change is required. The SDK reads URLSessionTaskMetrics.transactionMetrics from the URLSession task delegate and emits phase child spans under the parent HTTP span automatically.

Reused connections skip DNS, TCP, and TLS work. For those requests the SDK emits zero-duration child spans with l9rum.network.phase.skipped=true so the waterfall shape stays consistent.

API reference

Identify a user

L9Rum.shared.identify(userId: "user-123", attributes: [
"email": "user@example.com",
"plan": "premium",
])

Clear user on sign-out

L9Rum.shared.clearUser()

Capture errors

do {
try riskyOperation()
} catch {
L9Rum.shared.captureError(error, context: ["screen": "checkout"])
}

Track views (SwiftUI / Custom Navigation)

UIKit views are tracked automatically. For SwiftUI or custom navigation:

L9Rum.shared.startView("ProductDetailsScreen")
L9Rum.shared.setViewName("Product #42")

Custom events

L9Rum.shared.addEvent("purchase_completed", attributes: [
"product_id": "12345",
"amount": 29.99,
])

Global span attributes

// Inject attributes into every span
L9Rum.shared.spanAttributes([
"experiment": "checkout_v2",
"feature_flag": "new_cart",
])
// Clear
L9Rum.shared.spanAttributes(nil)

Session ID

let sessionId: String? = L9Rum.shared.getSessionId()

Flush pending data

L9Rum.shared.flush()

Network ignore patterns

Skip noisy URLs before span creation by matching against full URL, pathname, or hostname. .contains uses substring matching; .regex uses regex search semantics.

config.ignorePatterns = L9NetworkIgnorePatterns(
fullUrl: [
.contains("https://cdn.example.com"),
.regex("^https://.*\\.example\\.com", options: [.caseInsensitive]),
],
pathname: [
.contains(".pdf"),
.contains(".jpg"),
.regex("^/internal/metrics"),
],
hostname: [
.contains("cdn.example.com"),
.regex("(^|\\.)assets\\.example\\.com$", options: [.caseInsensitive]),
]
)
// PRESERVE (default): keep traceparent on ignored requests.
// STRIP: remove traceparent from ignored requests.
config.propagationMode = .preserve

WebView correlation

Inject the active native session and view IDs into a WKWebView so Browser RUM spans share the same session.id:

// After creating the WKWebView — call once per WKWebView instance.
L9Rum.shared.instrument(webView: webView)
  • Session and view IDs are re-injected on every navigation commit and view change.
  • Cross-origin iframes do not receive the session ID.
  • Calling instrument(webView:) before initialize(config:) emits a warning and is a no-op.

Requires the Browser RUM JS SDK on the page. The JS SDK adopts the native session ID and fires l9rum:session_rollover when the session rotates, rotating the view span accordingly.

See the WebView Session Correlation guide for React Native/Flutter setup, the static-script path, auto-load Browser RUM, and verification steps.

Security

Client monitoring tokens are write-only and origin-scoped to your app’s bundle ID. Safe to ship in the app binary.

Next steps

Once data is flowing, explore it in Discover > Applications — performance, errors, and sessions.


Troubleshooting

Please get in touch with us on Discord or Email if you have any questions.