PageTracker is the recommended approach for tracking user interactions in your iOS and tvOS apps. It automatically generates Pageview events, measures engagement time, and enables contextual cohorts when URLs are provided.
Overview
PageTracker provides:
Automatic Pageview events on first resume
Engagement time tracking between resume and pause
PageViewComplete events when stopped
Contextual cohort generation when URLs are provided
Linked event tracking with consistent view IDs
Only one PageTracker (or MediaTracker) can be active at a time. Creating a new tracker automatically stops any existing one.
Creating a PageTracker
import Permutive_iOS
class ArticleViewController : UIViewController {
private var pageTracker: PageTrackerProtocol?
override func viewDidLoad () {
super . viewDidLoad ()
// Create event properties (optional)
let properties = try ? EventProperties ([
"category" : "sports" ,
"subcategory" : "football" ,
"author" : "Jane Smith" ,
"premium" : true
])
// Create context with page information
let context = Context (
title : "Match Preview: Team A vs Team B" ,
url : URL ( string : "https://example.com/sports/match-preview" ),
referrer : URL ( string : "https://example.com/sports" )
)
// Create the PageTracker
pageTracker = try ? Permutive. shared . createPageTracker (
properties : properties,
context : context
)
}
}
PageTracker Lifecycle
State Diagram
┌──────────────────────────────────────────────────────────────┐
│ │
│ Created ──► resume() ──► Running ──► pause() ──► Paused │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ stop() ◄──────────────────┘ │
│ │ │
│ ▼ │
│ Stopped │
│ │
└──────────────────────────────────────────────────────────────┘
Lifecycle Methods
Method Effect Events Generated resume()Starts/resumes tracking Pageview (first call only)pause()Pauses engagement timer None stop()Ends tracking permanently PageViewComplete
UIViewController Integration
class ArticleViewController : UIViewController {
private var pageTracker: PageTrackerProtocol?
override func viewDidLoad () {
super . viewDidLoad ()
let context = Context (
title : "Article Title" ,
url : URL ( string : "https://example.com/article" ),
referrer : nil
)
pageTracker = try ? Permutive. shared . createPageTracker (
properties : nil ,
context : context
)
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear (animated)
// Start or resume tracking
try ? pageTracker?. resume ()
}
override func viewDidDisappear ( _ animated : Bool ) {
super . viewDidDisappear (animated)
// Pause when view is not visible
pageTracker?. pause ()
}
deinit {
// Stop tracking when view is deallocated
pageTracker?. stop ()
}
}
SwiftUI Integration
For SwiftUI, use onAppear and onDisappear modifiers:
import SwiftUI
import Permutive_iOS
struct ArticleView : View {
let article: Article
@StateObject private var tracker = PageTrackerWrapper ()
var body: some View {
ScrollView {
ArticleContent ( article : article)
}
. onAppear {
tracker. start (
title : article. title ,
url : URL ( string : article. url )
)
}
. onDisappear {
tracker. stop ()
}
}
}
class PageTrackerWrapper : ObservableObject {
private var pageTracker: PageTrackerProtocol?
func start ( title : String , url : URL?) {
let context = Context ( title : title, url : url, referrer : nil )
pageTracker = try ? Permutive. shared . createPageTracker (
properties : nil ,
context : context
)
try ? pageTracker?. resume ()
}
func stop () {
pageTracker?. stop ()
pageTracker = nil
}
}
Tracking Events
Track additional events linked to the current page view:
// Track a link click
try ? pageTracker?. track (
event : "LinkClick" ,
properties : try ? EventProperties ([
"link_url" : "https://example.com/related" ,
"link_text" : "Read More"
])
)
// Track a form submission
try ? pageTracker?. track (
event : "FormSubmit" ,
properties : try ? EventProperties ([
"form_id" : "newsletter_signup" ,
"email_provided" : true
])
)
// Track without properties
try ? pageTracker?. track ( event : "ShareButton" , properties : nil )
Events tracked via PageTracker.track() include the same Context and view ID as the Pageview event, enabling linked analysis.
Engagement Tracking
Track how much content users have viewed:
class ArticleViewController : UIViewController , UIScrollViewDelegate {
private var pageTracker: PageTrackerProtocol?
@IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad () {
super . viewDidLoad ()
scrollView. delegate = self
// Setup pageTracker...
}
func scrollViewDidScroll ( _ scrollView : UIScrollView) {
let contentHeight = scrollView. contentSize . height - scrollView. bounds . height
guard contentHeight > 0 else { return }
let percentage = Float (scrollView. contentOffset . y / contentHeight)
let clampedPercentage = min ( max (percentage, 0 ), 1 )
pageTracker?. updateViewed ( percentage : clampedPercentage)
}
}
Engagement Time
Engagement time is automatically tracked:
Timer starts when resume() is called
Timer pauses when pause() is called
Total engagement time sent with PageViewComplete event
Context Object
The Context object provides page metadata:
Property Type Description titleStringPage title urlURL?Page URL (enables contextual cohorts) referrerURL?Referring page URL
let context = Context (
title : "Article: iOS Development" ,
url : URL ( string : "https://example.com/ios-development" ),
referrer : URL ( string : "https://example.com/tutorials" )
)
The domain is automatically inferred from the url if set. Always provide URLs to enable contextual cohort generation.
Background Handling
The SDK automatically handles app backgrounding:
// When app enters background:
// - Active PageTracker is automatically stopped
// - PageViewComplete event is sent
// When resuming, create a new PageTracker if needed
func applicationWillEnterForeground () {
if shouldResumeTracking {
createAndResumePageTracker ()
}
}
tvOS Considerations
tvOS Note: PageTracker works identically on tvOS. Use focus-based navigation events instead of touch-based scrolling for engagement tracking.
// tvOS focus tracking example
func didUpdateFocus ( in context : UIFocusUpdateContext, with coordinator : UIFocusAnimationCoordinator) {
// Track focus changes as navigation events
if let focusedView = context.nextFocusedView {
try ? pageTracker?. track (
event : "FocusChange" ,
properties : try ? EventProperties ([
"focused_element" : String ( describing : type ( of : focusedView))
])
)
}
}
Error Handling
do {
let pageTracker = try Permutive. shared . createPageTracker (
properties : properties,
context : context
)
try pageTracker. resume ()
} catch {
print ( "PageTracker error: \( error ) " )
// Handle error - tracking will be skipped
}
Best Practices
Create PageTracker in viewDidLoad
Call resume() in viewDidAppear
Call pause() in viewDidDisappear
Call stop() in deinit
Always provide URLs for contextual targeting
Track meaningful events with descriptive names
Update scroll percentage as user scrolls
Don’t create multiple PageTrackers simultaneously
Don’t call resume() after stop()
Don’t forget to call stop() when done
Don’t track too many fine-grained events
Don’t include PII in event properties
Troubleshooting
Pageview event not firing
Problem: No Pageview event in logs.Solutions:
Ensure resume() is called after creating the PageTracker
Check that SDK is initialized before creating PageTracker
Verify no errors when creating PageTracker
Enable debug logging: options.logModes = LogMode.all
Previous PageTracker stopped unexpectedly
Problem: PageTracker stops when navigating.Cause: Creating a new PageTracker stops any existing one.Solution: This is expected behavior. Only one tracker can be active. Stop the previous tracker explicitly if needed, or let the new one replace it.
Problem: Events tracked via PageTracker are rejected.Solutions:
Check event names match your schema
Verify property names and types
Enable debug logging to see schema errors