pieowl

How to Add Analytics to Swift Apps with the HTTP API

Track events from your iOS app with the Pieowl HTTP API and URLSession. No SDK needed, just a small Swift helper.

Pieowl TeamBeginner · 10 min · 8 Oct 2026
Guide · iOS · Swift

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

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.

Analytics.swift
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.

MyApp.swift
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.

AppDelegate.swift
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.

SignupButton.swift
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.plan

Step 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.

AuthService.swift
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.

HomeView.swift
struct HomeView: View {
    var body: some View {
        VStack {
            Text("Home")
        }
        .onAppear {
            Analytics.screenView("HomeScreen")
        }
    }
}

In UIKit, override viewDidAppear in your view controllers.

HomeViewController.swift
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.

Your first dashboard in five minutes.

14-day free trial, no card needed. Cloud in the EU. No cookies.