Swift
Installation
Add Swift Package
Add Phase Analytics to your project using Swift Package Manager:
- In Xcode, go to File → Add Package Dependencies
- Enter the repository URL:
- Select the latest version
- Add to your target
Or add to Package.swift:
dependencies: [
.package(url: "https://github.com/Phase-Analytics/Phase-Swift", from: "0.2.0")
]Requirements:
- iOS 15.0+ or macOS 12.0+
- Swift 6.0+
Get Your API Key
- Sign in to Phase Dashboard
- Create a new project or select an existing one
- Open API Keys tab
- Copy your API Key (starts with
phase_)
Setup
SwiftUI
Wrap your app with the Phase view to initialize the SDK:
import SwiftUI
import PhaseAnalytics
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Phase(apiKey: "phase_xxx") {
ContentView()
}
}
}
}The SDK initialization is asynchronous. Call identify() to register the device and start a session. Events tracked before identify() runs (even before initialization finishes) are buffered and sent afterwards.
Configuration
The Phase view and initialize() method accept the following parameters:
Prop
Type
Usage
Identify User
Call identify() once at startup. This registers the device and starts a session. Calling it before initialization finishes is fine: it runs right after initialization.
import SwiftUI
import PhaseAnalytics
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.onAppear {
Task {
// Initialize analytics - no PII collected by default
await PhaseSDK.shared.identify()
}
}
}
}Privacy by default:
- No personal data is collected without explicit properties
- Device ID is auto-generated and stored locally
- Only technical metadata is collected (OS version, platform, locale, app version)
Adding custom properties:
You can optionally attach user properties. Important: If you add PII (personally identifiable information), ensure you have proper user consent:
// After user login and consent
await PhaseSDK.shared.identify([
"user_id": "123",
"plan": "premium",
"beta_tester": true
])
// ⚠️ If adding PII, get consent first
let hasConsent = await customGetUserConsent()
if hasConsent {
await PhaseSDK.shared.identify([
"email": "[email protected]",
"name": "John Doe"
])
}Properties must be flat primitives: String, Int, Double, Bool, or nil. Keys can be up to 128 characters. Invalid properties are logged and left out; the device is still identified.
Track Events
Track custom events with optional parameters. Events tracked before identify() runs are buffered (up to 200 events, for up to one hour) with their original timestamps and sent once the session exists. Each event gets a unique ID when it is tracked, so retries never create duplicates.
// Event without parameters
track("app_opened")
// Event with parameters
track("purchase_completed", [
"amount": 99.99,
"currency": "USD",
"product_id": "premium_plan"
])
// Using instance method
PhaseSDK.shared.track("button_clicked", params: ["button_id": "submit"])Event naming rules:
- Alphanumeric characters, underscores (
_), hyphens (-), periods (.), forward slashes (/), and spaces - 1-256 characters
- Examples:
purchase,user.signup,payment/success,Button Clicked
Event parameters:
- Flat primitive dictionary only
- Values must be
String,Int,Double,Bool, ornil - Max 32 keys
- Key max 32 characters
- String value max 256 characters
- Serialized payload max 8 KB
- Empty dictionaries are normalized away
NaNand infinite numbers are rejected
Screen Tracking
Use the .phaseScreen() modifier to automatically track screen views:
import SwiftUI
import PhaseAnalytics
struct ProfileView: View {
let userID: String
var body: some View {
VStack {
Text("Profile")
}
.phaseScreen("ProfileView", params: ["user_id": userID])
}
}How it works:
Tracks a screen view each time the view appears, including when navigating back to it. Screen names are normalized automatically (e.g., "ProfileView" → "/profile-view") with CamelCase converted to kebab-case; query strings are removed and unsupported characters become -. Supports optional parameters.
Flush
Data is sent automatically in batches (see Delivery). flush() sends queued data now, for example before a test assertion or a known shutdown, and returns when the attempt finishes. While offline or while the server asks the SDK to back off, data stays queued and is retried automatically.
await PhaseSDK.shared.flush()Reset
reset() forgets the current device, for example on logout. It ends the session, tries to deliver queued data, discards what could not be sent and generates a new device ID. Call identify() again to start tracking the new device.
await PhaseSDK.shared.reset()
await PhaseSDK.shared.identify()Type Reference
DeviceProperties
Custom user/device attributes passed to identify():
Prop
Type
EventParams
Event parameters passed to track() and trackScreen():
Prop
Type
How It Works
Delivery
Everything goes through a persistent queue and is sent in batches of up to 50 items:
- When 20 items are waiting, every 10 seconds while the queue is not empty, when the app moves to the background (inside a background task) and when connectivity returns
- Items leave the queue only after the server answers. Items the server rejects permanently are dropped; items that failed for a transient reason are retried up to 5 times
- Failed requests back off exponentially with jitter (up to 5 minutes) and honor
Retry-Afteron429/503. Requests time out after 15 seconds - Session heartbeats are coalesced to one pending heartbeat per session
Offline Support
The queue is stored in UserDefaults (up to 1000 items) and survives app restarts. Queued data is sent when connection is restored.
Privacy
- No personal data is collected by default
- Device IDs are generated locally and stored persistently
- Geolocation is resolved server-side from IP address (disable with properties)
- All data collection is optional via configuration
Performance
- Queued data is batched and sent asynchronously
- Network state is monitored automatically
- Failed requests retry with exponential backoff
- Maximum batch size: 50 items per request
- Queue writes to storage happen at most once per second (and immediately on backgrounding)
- Thread-safe with Swift 6 concurrency