Migrate on iOS
Preliminary pre-release documentation, subject to change without notice. The full list of properties on each event isn't published yet.
Updated on September 16, 2026.
The unified schema ships as a major version on iOS. Removed enum cases are deleted rather than deprecated, so the compiler points you at the call sites, and two identifiers that used to be optional are now required.
This page is written to be followed by a developer or a coding agent. Read Ask before you change these before you change code.
Read first: Migrate to the unified events schema holds the mapping tables this page assumes. This page adds only what's specific to iOS. The old value to new value pairs are only on that page, so don't change code from this page alone.
API reference: iOS
Before you start
- Wait for the SDK version. The unified schema ships in iOS 2.0.x. Don't start the code changes until you're upgrading to that line.
- Accept both schema versions. Your pipeline needs to accept
api_scheme_version"2"and"2.1"while older SDK versions in the field still emit legacy events. - Nothing is aliased. Old names aren't mapped to new ones anywhere in the SDK, so anything you miss goes silent rather than failing.
- The full list of properties on each event isn't published yet. Treat a mapping that isn't listed as unknown, not as unchanged. Don't run this as an unattended automated pass.
Ask before you change these
Don't make these changes without a person. The mapping tables say what the payload does now, not what your app should do about it. Compile-break rows in Fix what stops compiling are in scope. Rows below are not, even when they fail the build. Don't map a removed event to the nearest surviving case to make switch compile.
| Change | Why a person decides |
|---|---|
story_completed | It now means one Story finished, not a completed playlist. Code that treated it as playlist completion needs remaining_unread_count = 0, which is a logic change. |
| Removed events with no replacement | Forced play and pause, and cta_dismissed, are gone. Dropping the feature that depended on them is a product decision. search_suggestions_shown is gone. For suggestion click-through, count search_suggestions_clicked against search_opened. |
| Call to action metrics | cta_shown counts once per start rather than once per appearance, so thresholds and rates built on the old counting need rebasing. |
| Search attribution | Search identifiers ride every player event in the session, so counting logic has to move to unique search_session_id. |
widgetIdentifier values | The label groups your widget metrics, so choosing it is a naming decision, not a fix. Don't generate it per launch, and don't paste a name from the class, the file, or an example. |
Step 1: Find the affected code
The result of this step is a list of files to change, not a change.
- Search your app for each string in the table below.
- Search each Legacy cell in Migrate to the unified events schema: value remap, exit triggers, start triggers, renamed events, removed events, removed properties, and renamed properties.
- Search your dashboards, saved segments, and warehouse queries for the same strings. Code and reporting break independently, and reporting breaks silently.
The compiler won't catch string reads. Comparing event_action.rawValue, a category value, or a property name against a string literal keeps compiling and silently sees a different value. Review those by hand.
| Search for | Why it matters |
|---|---|
onEventTriggered | Handlers that read the payload |
event_category, event_action | Code that branches on event identity |
.rawValue on an analytics enum | String reads the compiler can't check |
playbackPause, playbackPlay, adPause, adResume | Renamed and removed enum cases |
playback_play, playback_pause, ad_playback_play, ad_playback_pause | Payload names for the same actions |
PlaybackActionMethod, PlayerViewingTransitionState, BlazeContainerEventProps, PlayableEndTrigger | Removed types |
forcedPlaybackPlay, forcedPlaybackPause, forced_playback_play, forced_playback_pause | Removed, no replacement. Report the call site. See Ask before you change these. |
ctaVisible, ctaDismissed, cta_visible, cta_dismissed | Removed. cta_dismissed has no replacement. See Ask before. |
search_click, container_data_load, video_orientation_changed | Renamed events |
moments_playlist_start, moments_playlist_exit, casting_started, casting_ended, pip_enabled, pip_disabled | Removed events |
search_suggestions_shown | Removed. For suggestion click-through, count search_suggestions_clicked against search_opened. See Ask before. |
swipeDown, Swipe Down | Removed exit trigger |
Stories completed, Moments Container | Values that don't recase 1:1. See the shared trigger tables. |
current_mode, next_mode | Replaced by a single viewing_mode string |
origin_widget_id, labels_expression, container_id | Moved referring and tab fields |
tabId: | Removed initializer parameter |
Widget and container initializers, and setup(with:) | The identifier arguments are now required. Don't invent the label. See Ask before. |
| Title Case string literals compared against property values | Search the Legacy column of the shared value remap. Don't recase and assume a match. |
Step 2: Fix what stops compiling
Removed cases are deleted outright. An exhaustive switch that stops compiling is the signal that an event you depended on is gone. If a compile error matches Ask before you change these, stop and report it.
| Was | Now |
|---|---|
BlazeContainerEventProps | Removed. BlazeTabEventProps replaces it, and unlike container properties it's exposed on BlazeAnalytics as .tab. |
viewing_mode, a PlayerViewingTransitionState object with current_mode and next_mode | PlayerViewingMode, a single value naming the mode being moved to. The object type is removed. |
.playbackPause / .playbackPlay | .userInitiatedPlaybackPause / .userInitiatedPlaybackPlay |
.adPause / .adResume | Removed. Ad pause and resume use the shared cases under category ad. |
BlazeAnalyticsModels.PlaybackActionMethod, with .press and .release | Removed. Anything referencing the type, not just the property, stops compiling. |
PlayerExitTriggers.swipeDown | Removed. Swipe to dismiss reports .userSwipeToDismiss, and swiping between playlists reports .swipe. |
PlayerExitTriggers and PlayerStartTriggers | New cases: .focusLost on exit, and .focusRegained and .autoAdvance on start. An exhaustive switch over either enum stops compiling until it handles them. |
PlayableEndTrigger | Removed. It was a public enum that no SDK API accepted or returned. BlazeAnalyticsModels.PlayerExitTriggers is the exit trigger vocabulary. |
BlazeVideoEventProps.stream_status, a String? | BlazeAnalyticsModels.StreamStatus?. Reading it as a String is a build error, not just a value change. |
BlazeMomentsPlayerContainer.init(…, tabId:) | The parameter is removed. Calls passing tabId: stop compiling. |
BlazeStoryEventProps.story_page_duration_viewed_percent, an Int? | Double?. Reading it as an Int is a build error. |
BlazeMomentEventProps.moment_duration_viewed_percent, an Int? | Double? |
BlazeWidgetEventProps.placement_distance, a String? | Double?. The tracking plan defines it as a number, and the SDK used to send a stringified integer. |
BlazeMomentEventProps.gesture_type, BlazeVideoEventProps.gesture_type | Removed. Those players have a single call to action gesture. BlazeStoryEventProps and BlazeAdEventProps keep it. |
BlazeWidgetEventProps.widget_name | Removed from the SDK and from the tracking plan. On iOS it duplicated widget_id exactly. |
BlazeAnalyticsModels.CtaConfig.type | Removed. It was declared and never populated, and the plan has no such field. |
widgetIdentifier, which defaulted to a timestamp | Required, and the first argument of every widget initializer and of setup(with:). See the next section. |
containerIdentifier, which defaulted to a timestamp | Required on BlazeMomentsPlayerContainer and BlazeVideosInlinePlayer. See the next section. |
Step 3: Required widget and container identifiers
widgetIdentifier used to be optional and defaulted to a timestamp string. That default was worse than no value: an integrator who never set it got a different widget ID on every launch, so widget analytics couldn't be aggregated across sessions at all. It's now a required argument.
// Before
let widget = BlazeStoriesWidgetRowView(layout: layout)
// Now
let widget = BlazeStoriesWidgetRowView(widgetIdentifier: "<placement-label>", layout: layout)The same applies to BlazeMomentsWidgetView, BlazeVideosWidgetView, their row and grid variants, the tabs-backed Moments initializer, and the SwiftUI configurations and widget view models.
setup(with:) changed the same way, for widgets built from a storyboard or from init(frame:):
// Before
widget.setup(with: layout)
// Now
widget.setup(widgetIdentifier: "<placement-label>", with: layout)Don't invent <placement-label>. Report each call site. The string is a naming decision, not a code fix. Don't paste a name from the class, the file, or this page. See Ask before you change these.
Pass a stable label for the placement, not for the run. Keep it identical across app launches and app versions, and use a different one for each placement, because widget metrics are grouped by this value. A widget built from a storyboard or init(frame:) and never given an identifier logs an error the first time it reports analytics.
containerIdentifier follows the same rule on BlazeMomentsPlayerContainer and BlazeVideosInlinePlayer. Both defaulted to a timestamp, and both feed referring.origin_source_id and the tab identity fields, so the generated default made those unstable across sessions. The SwiftUI inline configuration already required it.
Step 4: iOS data changes
These changes are specific to iOS. The schema is the same on every platform, and what differs here is where iOS reporting becomes more complete or more precise than it was. Nothing errors, so a report built on the previous behavior keeps running on the new numbers.
Fields that now carry a value
Reports that treated these fields as unused start seeing numbers. Some of these fields never arrived on iOS. Others arrived empty, or only on some events.
| Field | Was | Now |
|---|---|---|
story_page_type | Never sent from iOS | Sent on page-scoped Story events, and absent on story_start |
video_time_start, video_time_end | Never sent from iOS | video_time_start is the playhead where the current viewing began, and a resume keeps its position rather than reporting 0. video_time_end is the exit playhead. |
back_to_live_trigger | Not reported | Reported, and only on back_to_live |
moment_exit_trigger | Reported on a limited set of exit paths | Names the reason on every exit path |
ad_exit_trigger | Sent without a value | Reports why every Stories and Moments ad ended |
moment.localization | Absent | Sent on moment_start, matching Stories and Videos |
cc_state | Story and Moment only | Also on Video |
gesture_type on Story | Declared, but not populated | Populated on call to action and custom action button events |
thumbnail_format on widget_load | Only on widget_click | On both |
referring on Interaction events | Absent | interaction_view and interaction_answer carry it, so interaction engagement is attributable like every other player event |
Values that report differently
| What changed | What it means for your numbers |
|---|---|
story_page_navigation_type |
|
moment_navigation_type | Moment exits report automatic on auto-advance, not manual. |
story_page_duration_viewed_percent |
|
story_exit_trigger |
|
| Ad content identity |
|
moment.loop_number | It increments on every loop. |
| Casting transitions | Sent only when the casting state changes, so expect fewer of them. |
cc_on and cc_off | Only a real change reports. Re-selecting the active state, or changing caption language while captions stay on, reports nothing. |
| Moments seek | One moment.seek for a whole seek bar drag, not one per movement. |
| Preloaded banner ads | They attribute to the Story the banner was created for, not the Story on screen when the ad network calls back. |
| Composite feed sources |
|
tech device fields |
|
Behaviors that change event volume
| Behavior | What it means for your numbers |
|---|---|
widget_click fires on every real tap |
|
| Start and exit events strictly alternate |
|
app_foreground is a real app resume | In-app covers and returns report focus_lost and focus_regained. An "app resumed" count that included tab switches and web view dismissals drops. |
| Later Stories report how they were reached |
|
| Description events are capped per Moment view |
|
onEventTriggered is exactly-once |
|
Step 5: Verify
- Confirm
api_scheme_versionreads2.1in the payloads your app receives. - Confirm
event_categoryvalues arrive lowercase, and that your handler coverstabandglobal. - Confirm no handler still matches a legacy value. A handler that never fires is the failure mode here, so assert on a match rather than on the absence of errors.
- Confirm
widgetIdentifierandcontainerIdentifierare the stable placement labels you intended, and that they're identical across two launches of the app. - Confirm the properties you depend on are present on the events you read them from. A missing property means it doesn't apply to that event, not that it's null.
Related
- Migrate to the unified events schema: the payload reference for every platform
- Migrate on Android: the same job on Android
- Migrate on Web: the same job on Web
- Reporting changes to expect: metrics that move without anything failing
- 2026 events schema - breaking changes ahead: timeline
Updated 14 days ago
