Introduction
To improve a native Apple app, you need to know how people use it. This guide shows you how to send events, user profiles and screen views from Swift to Pieowl.
Pieowl has no Swift SDK. You don't need one. Tracking is one HTTP request to the Pieowl API, so a small helper built on URLSession does the job. The examples target iOS. The same request works on macOS, tvOS and watchOS. Only the device details change.
Prerequisites
- An Xcode project
- A Pieowl account (start a 14-day free trial)
- Your Client ID and Client Secret from the Pieowl dashboard
Important: Native apps need a
clientSecret. A browser sends an origin that Pieowl can check, but a native app does not.
Step 1: Add a tracking helper
Every call is a POST to https://api.pieowl.com/track with a JSON body. Two headers prove the request is yours: pieowl-client-id and pieowl-client-secret. The body always has a type and a payload.
Add this file to your project. It is your own code, so change it as you like.
import Foundation
import UIKit
@MainActor
enum Analytics {
static var apiURL = URL(string: "https://api.pieowl.com/track")!
static var clientId = ""
static var clientSecret = ""
static var profileId: String?
static var globalProperties: [String: Any] = [:]
// Tells Pieowl which device this is. Without these,
// events look like they come from a server.
static var deviceProperties: [String: Any] {
let device = UIDevice.current
return [
"__os": device.systemName,
"__osVersion": device.systemVersion,
"__device": device.userInterfaceIdiom == .pad ? "tablet" : "mobile",
"__brand": "Apple",
"__model": device.model,
]
}
static func send(type: String, payload: [String: Any]) {
var request = URLRequest(url: apiURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(clientId, forHTTPHeaderField: "pieowl-client-id")
request.setValue(clientSecret, forHTTPHeaderField: "pieowl-client-secret")
let body: [String: Any] = ["type": type, "payload": payload]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { _, response, error in
if let error {
print("[Analytics] \(error.localizedDescription)")
} else if let http = response as? HTTPURLResponse, http.statusCode != 200 {
print("[Analytics] HTTP \(http.statusCode)")
}
}.resume()
}
static func track(_ name: String, properties: [String: Any] = [:]) {
var props = deviceProperties
props.merge(globalProperties) { _, new in new }
props.merge(properties) { _, new in new }
var payload: [String: Any] = ["name": name, "properties": props]
if let profileId { payload["profileId"] = profileId }
send(type: "track", payload: payload)
}
static func identify(
profileId: String,
firstName: String? = nil,
lastName: String? = nil,
email: String? = nil,
properties: [String: Any] = [:]
) {
self.profileId = profileId
var payload: [String: Any] = ["profileId": profileId, "properties": properties]
if let firstName { payload["firstName"] = firstName }
if let lastName { payload["lastName"] = lastName }
if let email { payload["email"] = email }
send(type: "identify", payload: payload)
}
static func increment(_ property: String, by value: Int = 1) {
guard let profileId else { return }
send(type: "increment", payload: [
"profileId": profileId, "property": property, "value": value,
])
}
static func screenView(_ screen: String, properties: [String: Any] = [:]) {
var props = properties
props["__path"] = screen
track("screen_view", properties: props)
}
static func clear() {
profileId = nil
}
}The __os, __osVersion, __device, __brand and __model properties fill in the device columns in your dashboard.
Step 2: Set it up at launch
Set your keys early in your app's life, so the helper is ready before the first event.
For SwiftUI apps, do it in your App struct's initializer.
import SwiftUI
@main
struct MyApp: App {
init() {
Analytics.clientId = "YOUR_CLIENT_ID"
Analytics.clientSecret = "YOUR_CLIENT_SECRET"
Analytics.globalProperties = [
"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "",
]
Analytics.track("app_opened")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}For UIKit apps, do it in your AppDelegate's application(_:didFinishLaunchingWithOptions:) method.
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
Analytics.clientId = "YOUR_CLIENT_ID"
Analytics.clientSecret = "YOUR_CLIENT_SECRET"
Analytics.track("app_opened")
return true
}
}The client secret ships inside your app, and anyone who unpacks the app could read it. Create a separate "Write" client for this app in Settings → API keys. If the secret leaks, delete that client without touching your other apps.
Step 3: Track events
Call Analytics.track anywhere in your app. Each event has a name and optional properties.
import SwiftUI
struct SignupButton: View {
var body: some View {
Button("Sign Up") {
Analytics.track("button_clicked", properties: [
"button_name": "signup",
"button_location": "hero",
])
}
}
}Properties can be any keys and values that add context. Common events are form submits, purchases and feature use. Keep event names the same across your app. snake_case with a verb, like button_clicked, works well.
Set global properties
If some properties belong on every event, set them once in globalProperties. The helper adds them to each event.
Analytics.globalProperties["plan"] = user.planStep 4: Identify users
When a user signs in, call identify. It saves their profile and remembers their ID, so the events that follow are linked to them.
Analytics.identify(
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: [
"plan": user.plan,
"signup_date": user.createdAt.ISO8601Format(),
]
)profileId should be the user's ID from your own system. The name and email help you recognize users in your dashboard.
The helper keeps the ID in memory. When the app starts again, call identify again for a signed-in user.
Clear user data on logout
When a user logs out, call clear. New events are then not linked to the previous user.
func logout() {
Analytics.clear()
}Increment user properties
To keep a running count on a profile, such as logins or purchases, use increment. The profile must already exist.
Analytics.increment("login_count")Step 5: Track screen views
Screen views show you how users move through your app. The helper's screenView sends a screen_view event with the screen name as its path. Then it shows up in your pages and paths reports.
In SwiftUI, use the onAppear modifier.
struct HomeView: View {
var body: some View {
VStack {
Text("Home")
}
.onAppear {
Analytics.screenView("HomeScreen")
}
}
}In UIKit, override viewDidAppear in your view controllers.
class HomeViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Analytics.screenView("HomeScreen", properties: [
"screen_class": String(describing: type(of: self)),
])
}
}The helper is marked @MainActor, so call it from UI code or hop to the main actor first.
Verify your setup
Run your app in the simulator or on a device. Move between screens and tap a few buttons. Then open the Pieowl dashboard and check the realtime view.
Not seeing events?
- Check the Xcode console for
[Analytics]messages - HTTP 401 means the Client ID or Client Secret is wrong or missing
- HTTP 400 means the body is wrong, for example an empty event name
- Test on a stable network first
The Track API reference lists every event type and field.
Next steps
If you build a cross-platform app with React Native, use the React Native guide instead. That package has offline support built in. To learn how Pieowl handles privacy, read the cookieless analytics article.
Is there a Swift SDK?
No. Pieowl doesn't publish a Swift package. Tracking is a single HTTP request, so the small helper in this guide is all you need. You own that code and can extend it.
Why do I need a clientSecret for native apps?
A browser sends an origin that Pieowl checks against your site settings. A native app does not, so the client secret proves the events are yours. Use a separate client per app so you can remove it if the secret leaks.
Does this work with both SwiftUI and UIKit?
Yes. The helper only uses URLSession and UIDevice. Use the .onAppear modifier in SwiftUI or viewDidAppear in UIKit to track screen views.
Can I track events when the user is offline?
Not with this helper as written. If the request fails, the event is lost. To keep events while offline, save failed payloads and send them later. Add a __timestamp property with the original time as an ISO 8601 string, so the event keeps its real time.
Does Pieowl help with GDPR?
Pieowl is built for GDPR: no cookies, no stored IPs. Your data is stored in the EU. See the cookieless analytics article for details.