Docs

Events & Screen Tracking

Track custom events and screen views in your iOS app with the Grovs SDK

Custom events and screen tracking require Grovs iOS SDK 3.0 or later.

Tracking custom events

Use track to record an event with optional properties and tags:

Swift
Grovs.track("purchase", properties: ["item_id": "sku-42", "price": 19.99])
Grovs.track("level_complete", properties: ["level": 5, "score": 1200])
Grovs.track("button_tap")
Grovs.track("signup", tags: ["organic", "campaign-A"])

Events are persisted to disk and retried on failure. If the SDK has not finished authenticating, events are held in memory and sent once authentication completes — those held events are lost if the app is terminated first.

Every custom event automatically carries a screen_name property naming the most recently viewed screen, so events can be segmented by screen in the dashboard. A screen_name you pass yourself is never overwritten.

Event name rules

An event name must be non-empty and must not collide with a reserved system event name:

view, open, install, reinstall, app_open, time_spent, reactivation, user_referred, custom, screen_view

Use trackScreenView for screen views rather than track("screen_view").

Property values

Property values may be strings, numbers, booleans, NSNull, or nested arrays and dictionaries of those. The SDK sanitizes each value before sending:

  • Date, URL, and UUID are coerced to strings (dates use ISO 8601).
  • Values that cannot be represented in JSON — NaN, Infinity, or an unsupported object — are dropped for that key only. The remaining properties are still sent.
  • If the encoded properties exceed 8 KB, all properties are dropped and the event is still recorded.
  • The backend stores at most 64 property keys per event. Keys used for analytics filtering should be 256 bytes or less and contain no control characters.

Tags

Tags are capped at 20 per event, each up to 255 characters. Set global tags to attach them to every subsequent event:

Swift
Grovs.setGlobalTags(["beta", "experiment-A"])
 
Grovs.setGlobalTags(nil)  // clear

Global tags are merged with per-event tags. Per-event tags take priority — if the combined count exceeds 20, per-event tags are kept first and global tags fill the remaining slots.

Tracking screen views

Swift
Grovs.trackScreenView("Checkout", properties: ["section": "payment"])
Grovs.trackScreenView("Product Detail", properties: ["product_id": "abc-123"])

The screen_name key is injected into properties automatically. If the same screen name is sent twice within one second the duplicate is dropped, which keeps tab switches and orientation changes from flooding the dashboard.

Automatic screen tracking

autoTrackScreenViews defaults to true, so the SDK tracks screens on its own by hooking UIViewController.viewDidAppear. System controllers — navigation, tab bar, alerts, and similar — are filtered out, as are view controllers embedded as children of your own custom containers.

To turn it off, pass false when configuring:

Swift
Grovs.configure(
    APIKey: "your-api-key",
    useTestEnvironment: false,
    autoTrackScreenViews: false,
    delegate: self
)

Grovs.setSDK(enabled: false) also stops automatic screen tracking, along with every other kind of collection.

How a screen name is chosen

The SDK resolves names in this order, taking the first that produces one:

  1. Grovs.screenNameProvider, if you set one.
  2. A GrovsScreenTracking conformance on the view controller.
  3. For SwiftUI hosting controllers, the view type — CheckoutView becomes "Checkout".
  4. The cleaned class name — CheckoutViewController becomes "Checkout".

SwiftUI screens whose name cannot be resolved unambiguously — an AnyView root, for example — are dropped rather than reported under a meaningless name. Annotate those explicitly with .grovsScreen().

SwiftUI

The .grovsScreen() modifier names a screen explicitly, and suppresses automatic tracking for the enclosing hosting controller so the screen is never counted twice:

Swift
struct CheckoutView: View {
    var body: some View {
        VStack {
            // ...
        }
        .grovsScreen("Checkout")
    }
}

Called with no name, it derives one from the file nameCheckoutView.swift becomes "Checkout":

Swift
.grovsScreen()

The automatic name comes from the file, not the view type. If several views share a file, or the file name does not match the screen, pass an explicit name.

If you drive navigation from your own router — swapping screens inside a single hosting controller — those transitions are invisible to automatic tracking. Add .grovsScreen() to each screen.

UIKit

Conform to GrovsScreenTracking to name a specific controller, or return nil to skip it:

Swift
extension CheckoutViewController: GrovsScreenTracking {
    var grovsScreenName: String? { "Checkout" }
}

Custom resolver

Grovs.screenNameProvider runs before every other rule and covers all view controllers at once:

Swift
Grovs.screenNameProvider = { viewController in
    switch viewController {
    case is LegalViewController:
        return .suppress
    case let vc as ProductViewController:
        return .name("Product \(vc.categoryName)")
    default:
        return .automatic
    }
}

Return .name(_:) to override the name, .suppress to skip the controller, or .automatic to fall through to the SDK's own resolution. The closure is called on the main thread — keep it fast, and do not retain the view controller.

Visibility filtering runs first, so controllers that are not the frontmost screen, and controllers annotated with .grovsScreen(), never reach the resolver.

Screen name aliases

Map view controller class names to friendlier names in the dashboard:

Swift
Grovs.setScreenAliases([
    "CheckoutViewController": "Checkout Page",
    "PDPViewController": "Product Detail"
])

Aliases are synced to the backend after authentication and retried on the next app foreground if the sync fails.

Next steps

Edit this page on GitHubLast updated 2026-09-03