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:
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, andUUIDare 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:
Grovs.setGlobalTags(["beta", "experiment-A"])
Grovs.setGlobalTags(nil) // clearGlobal 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
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:
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:
Grovs.screenNameProvider, if you set one.- A
GrovsScreenTrackingconformance on the view controller. - For SwiftUI hosting controllers, the view type —
CheckoutViewbecomes"Checkout". - The cleaned class name —
CheckoutViewControllerbecomes"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:
struct CheckoutView: View {
var body: some View {
VStack {
// ...
}
.grovsScreen("Checkout")
}
}Called with no name, it derives one from the file name — CheckoutView.swift becomes "Checkout":
.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:
extension CheckoutViewController: GrovsScreenTracking {
var grovsScreenName: String? { "Checkout" }
}Custom resolver
Grovs.screenNameProvider runs before every other rule and covers all view controllers at once:
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:
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.