---
title: "How to add product analytics to an iOS app with Swift"
description: "Track SwiftUI report opens and date-range changes with Amplitude. Initialize one shared client and connect your existing sign-in flow."
canonical: "https://amplitude.com/en-us/build/ios-product-analytics"
---

# How to add product analytics to an iOS app with Swift

Track SwiftUI report opens and date-range changes with Amplitude. Initialize one shared client and connect your existing sign-in flow.

Source: https://amplitude.com/en-us/build/ios-product-analytics

---

# How to add product analytics to an iOS app with Swift

Track report opens and date-range changes in a SwiftUI app with Amplitude's Unified SDK. Allow about 25 minutes after Xcode and an iOS simulator are installed.

- iOS 17+
- SwiftUI
- Unified Swift SDK 0.0.4
- 25 min

Chris Tam · Last updated 27 October 2026

SwiftUI apps can keep one analytics client shared across their views. [Amplitude product analytics](https://amplitude.com/amplitude-analytics) turns those interactions into events you can inspect by device, account, and report date range.

In this tutorial, you'll build a small usage-report screen, track its two buttons, connect an existing sign-in flow, and find the resulting events in Amplitude.

An iOS report screen that sends Report Previewed and Report Range Changed with report\_type, days, and platform. The app shows its device ID so you can find the same events in Amplitude, then verify account attribution when you connect authentication.

## Prerequisites

- Xcode with an iOS 17+ simulator or device. The sample uses SwiftUI and Swift 5 language mode; use the SwiftUI App template. The SDK package requires Swift tools 5.9 or later.
- A project API key from [Organization settings > API Keys](https://amplitude.com/docs/admin/account-management/manage-your-api-keys-and-secret-keys). Select the project and note its US or EU region.
- For signed-in checks, an existing authentication system and the same canonical account ID used by your backend, at least five characters long.

No Amplitude account yet? A free account is enough for the explicit events in this tutorial. [Sign up for free](https://app.amplitude.com/signup) to get your project API key.

## 1. Create the SwiftUI app and add the SDK

I create an iOS App project named AmplitudeReports in Xcode, with interface SwiftUI, language Swift, and no persistence framework. Set the deployment target to iOS 17 and the Swift language version to Swift 5. Keep Xcode's generated project, asset catalog, bundle identifier and signing settings. The nine custom files on this page supply the complete sample; no sibling tutorial is required.

The [complete SwiftUI example](https://github.com/amplitude/ampli-examples/tree/cfeeb9273aeb5eda82a4d6fbb4a17d520459c603/ios/unified-swift-reports) includes the app files, project generator, locked dependencies and tests.

In an existing app, merge the dependency, configuration and event calls into its current structure. Preserve its app entry point, authentication, navigation, build settings and other instrumentation.

Following the [mobile installation guide](https://amplitude.com/docs/get-started/install-mobile-sdk#ios), choose File > Add Package Dependencies and enter:

**Xcode package URL**

Xcode package URL

```text
https://github.com/amplitude/AmplitudeUnified-Swift
```

Choose Exact Version 0.0.4 and add the AmplitudeUnified product to the AmplitudeReports app target. Keep the Package.resolved file that Xcode creates so the resolved dependency versions travel with the project. Don't add another analytics client alongside this one.

The Unified package includes analytics, experiment support and the iOS replay plugin. I explicitly keep replay sampling, remote replay overrides and automatic capture off in step 3; this page adds event tracking without starting a recording.

## 2. Read the project key from the build configuration

I add a Config folder at the project root and copy Local.xcconfig.example to Local.xcconfig. Enter the project API key in that local file and set AMPLITUDE\_SERVER\_ZONE to US or EU. Keep the example blank in source control. A mobile app's project API key ships in the app bundle; use the ingestion API key, never a secret key or server deployment key.

**Config/Amplitude.xcconfig**

Config/Amplitude.xcconfig

```text
#include? "Local.xcconfig"
GENERATE_INFOPLIST_FILE = NO
INFOPLIST_FILE = AmplitudeReports/Info.plist
INFOPLIST_EXPAND_BUILD_SETTINGS = YES
```

**Config/Local.xcconfig.example**

Config/Local.xcconfig.example

```text
AMPLITUDE_API_KEY =
AMPLITUDE_SERVER_ZONE = US
```

**.gitignore**

.gitignore

```text
.build/
.swiftpm/
Config/Local.xcconfig
xcuserdata/
```

In the project editor's Info > Configurations table, select Config/Amplitude.xcconfig as the app target's base configuration for both Debug and Release. In an existing project, include these settings from its current configuration instead of replacing it.

In the app target's Build Settings, inspect both Debug and Release. Target-level values override an xcconfig: remove any target-level overrides for GENERATE\_INFOPLIST\_FILE and INFOPLIST\_FILE so they inherit the configuration, or explicitly set them to NO and AmplitudeReports/Info.plist. Confirm those two resolved values and set Expand Build Settings in Info.plist File (INFOPLIST\_EXPAND\_BUILD\_SETTINGS) to YES before building, so the AMPLITUDE variables are substituted. Apple explains this precedence in [adding a build configuration file](https://developer.apple.com/documentation/xcode/adding-a-build-configuration-file-to-your-project).

Add this Info.plist file at AmplitudeReports/Info.plist. The configuration points to it and disables generated Info.plist output. Keep the file out of Copy Bundle Resources; Xcode processes it as the target's information property list. Merge only the two AMPLITUDE keys into an existing app's plist.

**AmplitudeReports/Info.plist**

AmplitudeReports/Info.plist

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key><string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
    <key>CFBundleName</key><string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>CFBundleShortVersionString</key><string>$(MARKETING_VERSION)</string>
    <key>CFBundleVersion</key><string>$(CURRENT_PROJECT_VERSION)</string>
    <key>LSRequiresIPhoneOS</key><true/>
    <key>UIApplicationSceneManifest</key>
    <dict><key>UIApplicationSupportsMultipleScenes</key><false/></dict>
    <key>UILaunchScreen</key><dict/>
    <key>UISupportedInterfaceOrientations</key>
    <array><string>UIInterfaceOrientationPortrait</string></array>
    <key>AMPLITUDE_API_KEY</key><string>$(AMPLITUDE_API_KEY)</string>
    <key>AMPLITUDE_SERVER_ZONE</key><string>$(AMPLITUDE_SERVER_ZONE)</string>
</dict>
</plist>
```

**AmplitudeReports/Core/AnalyticsSettings.swift**

AmplitudeReports/Core/AnalyticsSettings.swift

```swift
import Foundation

struct AnalyticsSettings {
    enum Zone: String { case US, EU }
    let apiKey: String
    let zone: Zone

    init?(info: [String: Any]) {
        let key = (info["AMPLITUDE_API_KEY"] as? String ?? "")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        let region = (info["AMPLITUDE_SERVER_ZONE"] as? String ?? "US")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        guard !key.isEmpty, !key.contains("$("), let zone = Zone(rawValue: region) else {
            return nil
        }
        self.apiKey = key
        self.zone = zone
    }
```

The settings guard leaves analytics off when the key is blank, a build variable is unresolved, or the region is invalid. Rebuild after changing the xcconfig so the values reach the app bundle.

## 3. Initialize one shared client and connect account callbacks

I keep the client in an app-wide service, outside View\.body and view appearance callbacks. All calls from the UI and authentication layer use this main-actor service. Explicit event calls keep the example independent of automatic view tracking; remote autocapture configuration is disabled too.

**AmplitudeReports/Core/ReportAnalytics.swift**

AmplitudeReports/Core/ReportAnalytics.swift

```swift
import AmplitudeUnified
import Foundation

@MainActor
final class ReportAnalytics {
    static let shared = ReportAnalytics(settings: AnalyticsSettings(info: Bundle.main.infoDictionary ?? [:]))
    private let client: Amplitude?
    var configured: Bool { client != nil }

    convenience init(settings: AnalyticsSettings?) {
        guard let settings else {
            self.init(client: nil)
            return
        }
        let analytics = AnalyticsConfig(autocapture: [], enableAutoCaptureRemoteConfig: false)
        #if os(iOS)
        let replay = SessionReplayPlugin.Config(
            sampleRate: 0,
            enableRemoteConfig: false,
            autoStart: false
        )
        let client = Amplitude(
            apiKey: settings.apiKey,
            serverZone: settings.zone == .EU ? .EU : .US,
            analyticsConfig: analytics,
            sessionReplayConfig: replay
        )
        #else
        // The same event/auth helper can be checked on macOS; replay is iOS-only.
        let client = Amplitude(
            apiKey: settings.apiKey,
            serverZone: settings.zone == .EU ? .EU : .US,
            analyticsConfig: analytics
        )
        #endif
        self.init(client: client)
    }

    init(client: Amplitude?) { self.client = client }

    func reportPreviewed(days: Int) {
        client?.track(eventType: "Report Previewed", eventProperties: properties(days: days))
    }

    func reportRangeChanged(days: Int) {
        client?.track(eventType: "Report Range Changed", eventProperties: properties(days: days))
    }

    // Call after the existing authentication system confirms the account.
    func accountSignedIn(userID: String, plan: String) {
        precondition(userID.count >= 5, "Use your canonical user ID, at least five characters")
        client?.setUserId(userId: userID)
        let identify = Identify()
        identify.set(property: "plan", value: plan)
        client?.identify(identify: identify)
    }

    // Call only after confirmed logout, never for an anonymous/loading auth state.
    func accountSignedOut() {
        client?.reset()
        // A fresh UUID separates logged-out events even when IDFV is available.
        client?.setDeviceId(deviceId: UUID().uuidString)
    }
    func flush() { client?.flush() }
    func deviceID() -> String? { client?.getDeviceId() }

    private func properties(days: Int) -> [String: Any] {
        ["report_type": "usage", "days": days, "platform": "iOS"]
    }
```

After your existing authentication confirms a sign-in, call ReportAnalytics.shared.accountSignedIn(userID:plan:) before tracking actions for that account. Use the same canonical ID as the backend. Call accountSignedOut() only after confirmed logout, before the next anonymous action. An anonymous or loading auth state doesn't call reset. The fresh sample runs anonymously until you connect those callbacks.

**Why.** I set the plan before the account's next report event so that event can carry the updated user property. The service resets identity only at a confirmed logout boundary, then explicitly assigns a fresh UUID device ID before the next event. This keeps logged-out events separate even when the SDK can reuse Apple's identifier for vendors (IDFV).

**MCP.** Your agent can use [Amplitude's MCP server](https://amplitude.com/mcp) to inspect the project's existing taxonomy before choosing event names. Keep existing names when you merge this sample into an instrumented app.

## 4. Track the two report actions

I track each action in its button handler. SwiftUI can evaluate a view's body repeatedly; rendering the same report isn't another button action. The sample's numbers are local demo data.

**AmplitudeReports/ReportModel.swift**

AmplitudeReports/ReportModel.swift

```swift
import Combine
import Foundation

@MainActor
final class ReportModel: ObservableObject {
    @Published private(set) var days = 7
    @Published private(set) var opened = false
    @Published private(set) var status: String
    let analytics: ReportAnalytics

    init(analytics: ReportAnalytics? = nil) {
        let analytics = analytics ?? .shared
        self.analytics = analytics
        status = analytics.configured
            ? "Analytics configured; verify delivery separately"
            : "Analytics off: check the API key and region in the build configuration"
    }

    var report: String {
        opened ? "\(days)-day report: \(days * 12) sample actions" : "Open the sample report"
    }
    func openReport() {
        opened = true
        analytics.reportPreviewed(days: days)
    }
    func changeRange() {
        days = days == 7 ? 30 : 7
        opened = true
        analytics.reportRangeChanged(days: days)
    }
    func flushAndShowDevice() {
        analytics.flush()
        status = "Device ID: \(analytics.deviceID() ?? "not available yet"); flush requested"
    }
```

**AmplitudeReports/ContentView\.swift**

AmplitudeReports/ContentView\.swift

```swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var model: ReportModel

    var body: some View {
        NavigationStack {
            VStack(alignment: .leading, spacing: 16) {
                Text(model.status).font(.footnote).textSelection(.enabled)
                Text(model.report)
                Button("Open report", action: model.openReport)
                Button("Change date range", action: model.changeRange)
                Button("Flush and show device ID", action: model.flushAndShowDevice)
                Spacer()
            }
            .padding()
            .navigationTitle("Usage reports")
        }
```

The first button emits Report Previewed with days 7. Changing the range emits Report Range Changed and switches days between 30 and 7. Flush and show device ID requests an upload without adding another report event.

## 5. Own the model at the app entry point

I replace the fresh template's app and ContentView files with the versions on this page, keeping only one @main entry point. Add the Core and model files to the app target. The app owns its model with StateObject and passes that model into the view. A background transition requests a flush; flush is asynchronous and doesn't guarantee delivery before iOS suspends or terminates the process.

**AmplitudeReports/AmplitudeReportsApp.swift**

AmplitudeReports/AmplitudeReportsApp.swift

```swift
import SwiftUI

@main
@MainActor
struct AmplitudeReportsApp: App {
    @StateObject private var model = ReportModel()
    @Environment(\.scenePhase) private var phase

    var body: some Scene {
        WindowGroup { ContentView(model: model) }
            .onChange(of: phase) { _, next in
                if next == .background { model.analytics.flush() }
            }
```

Select the app scheme and an iOS simulator, then choose Product > Build and run the app. In an existing application, merge the shared model and lifecycle hook into its current App or scene structure.

Before distributing your app, review its privacy disclosures and the SDK packages' privacy manifests against your actual data collection. The SDK's default manifest doesn't describe every app-specific event property you choose to send. See the [Swift SDK privacy guidance](https://amplitude.com/docs/sdks/analytics/ios/ios-swift-sdk#apple-privacy-manifest).

## Verify it worked

Run with a development project key. Tap Open report once, Change date range once, then Flush and show device ID. Keep the app open while you check:

1. **App state.** The report changes from 7 to 30 days and the status shows a device ID. A configured client or a requested flush alone doesn't prove delivery.
2. **Xcode logs and ingestion.** Check for SDK/network errors, then use the [Ingestion Debugger](https://amplitude.com/docs/analytics/debug-analytics#ingestion-debugger) to check successful requests and throttling for the project. Allow time for queued events and downstream processing.
3. **User Profiles.** Find the exact device ID in Amplitude's User Profiles and open its event stream. Confirm one Report Previewed and one Report Range Changed from this interaction, with report\_type usage, platform iOS, and days 7 then 30.
4. **Account attribution.** With your real auth connected, sign in and open a report. Find the canonical user ID and confirm plan on that event. After confirmed logout, open another report and verify the event uses the new anonymous device identity. Don't treat a code review as proof of this flow.
5. **Capture scope.** Keep the explicit replay configuration unchanged for this analytics-only app. Verify the effective capture configuration before adding recording or real sensitive content in another tutorial.

## Troubleshooting

### The app says analytics is off

Check the app target's base configuration, the local xcconfig filename, the two plist values and the exact US/EU region. Rebuild the app; editing a local file doesn't change an already installed bundle.

### The compiler cannot find AmplitudeUnified

Add the AmplitudeUnified package product to the app target and resolve version 0.0.4. Keep the generated Package.resolved. A package listed in the project without a linked product isn't enough.

### Events appear more than once

Keep track calls in button handlers, reuse the shared client, and check for duplicate manual calls or other instrumentation in the existing app. This sample disables autocapture and its remote overrides.

### Events are queued but not visible yet

Check the API key's project and region, device connectivity, ingestion errors and throttling. Request a flush and keep the app open. User Profiles and analytics queries can take longer to reflect events than the upload response.

### The account changes while authentication is loading

Remove reset calls from generic nil-user observers. Connect the account callbacks to confirmed sign-in and sign-out events. Don't use an email address or a changing presentation name when your backend already has a canonical account ID.

## Frequently asked questions

### Does this record the screen?

The sample passes autoStart false, sampleRate 0 and enableRemoteConfig false to replay configuration and never calls start. Keep those settings until you add and verify a separate consent-aware recording flow.

### Can I use this in a UIKit app?

The analytics calls are the same, but this walkthrough's app entry point and UI use SwiftUI. Merge the shared service into your UIKit lifecycle instead of adding a second application entry point.

### Is flush a completion signal?

No. It requests an asynchronous upload. Check ingestion and the processed user event stream before treating delivery as verified.

## Full code reference

Nine custom files. Keep the generated Xcode project and assets alongside them. The local key file is created from the blank example and stays outside source control.

**Complete sample**

Complete sample

```text
// Config/Amplitude.xcconfig
#include? "Local.xcconfig"
GENERATE_INFOPLIST_FILE = NO
INFOPLIST_FILE = AmplitudeReports/Info.plist
INFOPLIST_EXPAND_BUILD_SETTINGS = YES

// Config/Local.xcconfig.example
AMPLITUDE_API_KEY =
AMPLITUDE_SERVER_ZONE = US

// .gitignore
.build/
.swiftpm/
Config/Local.xcconfig
xcuserdata/

// AmplitudeReports/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key><string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
    <key>CFBundleName</key><string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>CFBundleShortVersionString</key><string>$(MARKETING_VERSION)</string>
    <key>CFBundleVersion</key><string>$(CURRENT_PROJECT_VERSION)</string>
    <key>LSRequiresIPhoneOS</key><true/>
    <key>UIApplicationSceneManifest</key>
    <dict><key>UIApplicationSupportsMultipleScenes</key><false/></dict>
    <key>UILaunchScreen</key><dict/>
    <key>UISupportedInterfaceOrientations</key>
    <array><string>UIInterfaceOrientationPortrait</string></array>
    <key>AMPLITUDE_API_KEY</key><string>$(AMPLITUDE_API_KEY)</string>
    <key>AMPLITUDE_SERVER_ZONE</key><string>$(AMPLITUDE_SERVER_ZONE)</string>
</dict>
</plist>

// AmplitudeReports/Core/AnalyticsSettings.swift
import Foundation

struct AnalyticsSettings {
    enum Zone: String { case US, EU }
    let apiKey: String
    let zone: Zone

    init?(info: [String: Any]) {
        let key = (info["AMPLITUDE_API_KEY"] as? String ?? "")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        let region = (info["AMPLITUDE_SERVER_ZONE"] as? String ?? "US")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        guard !key.isEmpty, !key.contains("$("), let zone = Zone(rawValue: region) else {
            return nil
        }
        self.apiKey = key
        self.zone = zone
    }

// AmplitudeReports/Core/ReportAnalytics.swift
import AmplitudeUnified
import Foundation

@MainActor
final class ReportAnalytics {
    static let shared = ReportAnalytics(settings: AnalyticsSettings(info: Bundle.main.infoDictionary ?? [:]))
    private let client: Amplitude?
    var configured: Bool { client != nil }

    convenience init(settings: AnalyticsSettings?) {
        guard let settings else {
            self.init(client: nil)
            return
        }
        let analytics = AnalyticsConfig(autocapture: [], enableAutoCaptureRemoteConfig: false)
        #if os(iOS)
        let replay = SessionReplayPlugin.Config(
            sampleRate: 0,
            enableRemoteConfig: false,
            autoStart: false
        )
        let client = Amplitude(
            apiKey: settings.apiKey,
            serverZone: settings.zone == .EU ? .EU : .US,
            analyticsConfig: analytics,
            sessionReplayConfig: replay
        )
        #else
        // The same event/auth helper can be checked on macOS; replay is iOS-only.
        let client = Amplitude(
            apiKey: settings.apiKey,
            serverZone: settings.zone == .EU ? .EU : .US,
            analyticsConfig: analytics
        )
        #endif
        self.init(client: client)
    }

    init(client: Amplitude?) { self.client = client }

    func reportPreviewed(days: Int) {
        client?.track(eventType: "Report Previewed", eventProperties: properties(days: days))
    }

    func reportRangeChanged(days: Int) {
        client?.track(eventType: "Report Range Changed", eventProperties: properties(days: days))
    }

    // Call after the existing authentication system confirms the account.
    func accountSignedIn(userID: String, plan: String) {
        precondition(userID.count >= 5, "Use your canonical user ID, at least five characters")
        client?.setUserId(userId: userID)
        let identify = Identify()
        identify.set(property: "plan", value: plan)
        client?.identify(identify: identify)
    }

    // Call only after confirmed logout, never for an anonymous/loading auth state.
    func accountSignedOut() {
        client?.reset()
        // A fresh UUID separates logged-out events even when IDFV is available.
        client?.setDeviceId(deviceId: UUID().uuidString)
    }
    func flush() { client?.flush() }
    func deviceID() -> String? { client?.getDeviceId() }

    private func properties(days: Int) -> [String: Any] {
        ["report_type": "usage", "days": days, "platform": "iOS"]
    }

// AmplitudeReports/ReportModel.swift
import Combine
import Foundation

@MainActor
final class ReportModel: ObservableObject {
    @Published private(set) var days = 7
    @Published private(set) var opened = false
    @Published private(set) var status: String
    let analytics: ReportAnalytics

    init(analytics: ReportAnalytics? = nil) {
        let analytics = analytics ?? .shared
        self.analytics = analytics
        status = analytics.configured
            ? "Analytics configured; verify delivery separately"
            : "Analytics off: check the API key and region in the build configuration"
    }

    var report: String {
        opened ? "\(days)-day report: \(days * 12) sample actions" : "Open the sample report"
    }
    func openReport() {
        opened = true
        analytics.reportPreviewed(days: days)
    }
    func changeRange() {
        days = days == 7 ? 30 : 7
        opened = true
        analytics.reportRangeChanged(days: days)
    }
    func flushAndShowDevice() {
        analytics.flush()
        status = "Device ID: \(analytics.deviceID() ?? "not available yet"); flush requested"
    }

// AmplitudeReports/ContentView.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var model: ReportModel

    var body: some View {
        NavigationStack {
            VStack(alignment: .leading, spacing: 16) {
                Text(model.status).font(.footnote).textSelection(.enabled)
                Text(model.report)
                Button("Open report", action: model.openReport)
                Button("Change date range", action: model.changeRange)
                Button("Flush and show device ID", action: model.flushAndShowDevice)
                Spacer()
            }
            .padding()
            .navigationTitle("Usage reports")
        }

// AmplitudeReports/AmplitudeReportsApp.swift
import SwiftUI

@main
@MainActor
struct AmplitudeReportsApp: App {
    @StateObject private var model = ReportModel()
    @Environment(\.scenePhase) private var phase

    var body: some Scene {
        WindowGroup { ContentView(model: model) }
            .onChange(of: phase) { _, next in
                if next == .background { model.analytics.flush() }
            }
```

## Hand it to your agent

Use this prompt with your coding agent.

**Agent prompt**

Agent prompt

```text
Follow https://amplitude.com/build/ios-product-analytics. Create the complete iOS 17+ SwiftUI usage-report sample, or merge it into the existing app while preserving authentication, navigation, privacy settings, build configurations and instrumentation. Use AmplitudeUnified-Swift exactly 0.0.4 and one shared client. Add the local xcconfig and plist substitutions without committing a real key; match its US/EU region. Keep track calls in explicit buttons, outside View.body and appearance callbacks. Keep autocapture and remote autocapture changes disabled. Keep replay autoStart false, sampleRate 0 and enableRemoteConfig false, and never start capture for this page. Connect canonical account IDs and plan only after confirmed sign-in; reset and assign a fresh UUID device ID only on confirmed logout, before tracking another event. Preserve the event names and properties. Treat flush as asynchronous. Report syntax, compilation, simulator/device behavior and live service delivery separately. Do not claim mocked success or a displayed device ID proves processed ingestion.
```

## What this unlocks

[Product analytics](https://amplitude.com/amplitude-analytics) compares Report Previewed by days and plan. After separate setup, [feature flags](https://amplitude.com/feature-management) can vary report presentation, [session replay](https://amplitude.com/session-replay) can add consent-aware visual context, and [guides and surveys](https://amplitude.com/guides-and-surveys) can explain the report or collect feedback. Each product needs its own configuration and verification.

## Next steps

[MobileAdd analytics to an Android appUse the same event names in a native Kotlin app.](https://amplitude.com/build/android-product-analytics)[MobileAdd analytics to React NativeCompare the cross-platform integration.](https://amplitude.com/build/react-native-product-analytics)[PlatformDebug analyticsFollow request ingestion to individual profile events.](https://amplitude.com/docs/analytics/debug-analytics)

Reference docs

[Install a mobile SDK →](https://amplitude.com/docs/get-started/install-mobile-sdk#ios)[Unified SDK for Swift →](https://amplitude.com/docs/sdks/analytics/ios/unified-sdk)[iOS Swift SDK →](https://amplitude.com/docs/sdks/analytics/ios/ios-swift-sdk)[Replay plugin configuration →](https://amplitude.com/docs/sdks/session-replay/session-replay-ios-plugin#configuration)[Debug analytics →](https://amplitude.com/docs/analytics/debug-analytics)[SwiftUI StateObject →](https://developer.apple.com/documentation/swiftui/stateobject)

Amplitude is the leading [all-in-one analytics platform](https://amplitude.com/) for building better products. Product and engineering teams use Amplitude to connect [product analytics](https://amplitude.com/amplitude-analytics) and [web analytics](https://amplitude.com/web-analytics) with [session replay](https://amplitude.com/session-replay), [heatmaps](https://amplitude.com/heatmaps), [experimentation](https://amplitude.com/amplitude-experiment), [feature flags](https://amplitude.com/feature-management), and [guides and surveys](https://amplitude.com/guides-and-surveys) in one unified workflow, with [AI agents](https://amplitude.com/ai-agents) working across all of it. Learn how Amplitude is building [self-improving products](https://amplitude.com/self-improving-products) with [Wave](https://amplitude.com/wave), helping teams turn product data into PRs.

For agents

[View raw .md](/build/ios-product-analytics.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

Use verified report interactions to decide what to improve next.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
