GuidesAPI ReferenceRelease Notes
HomeLog InHome
Guides

Migrate on iOS

Heads up:

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.

ChangeWhy a person decides
story_completedIt 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 replacementForced 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 metricscta_shown counts once per start rather than once per appearance, so thresholds and rates built on the old counting need rebasing.
Search attributionSearch identifiers ride every player event in the session, so counting logic has to move to unique search_session_id.
widgetIdentifier valuesThe 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.

  1. Search your app for each string in the table below.
  2. 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.
  3. 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 forWhy it matters
onEventTriggeredHandlers that read the payload
event_category, event_actionCode that branches on event identity
.rawValue on an analytics enumString reads the compiler can't check
playbackPause, playbackPlay, adPause, adResumeRenamed and removed enum cases
playback_play, playback_pause, ad_playback_play, ad_playback_pausePayload names for the same actions
PlaybackActionMethod, PlayerViewingTransitionState, BlazeContainerEventProps, PlayableEndTriggerRemoved types
forcedPlaybackPlay, forcedPlaybackPause, forced_playback_play, forced_playback_pauseRemoved, no replacement. Report the call site. See Ask before you change these.
ctaVisible, ctaDismissed, cta_visible, cta_dismissedRemoved. cta_dismissed has no replacement. See Ask before.
search_click, container_data_load, video_orientation_changedRenamed events
moments_playlist_start, moments_playlist_exit, casting_started, casting_ended, pip_enabled, pip_disabledRemoved events
search_suggestions_shownRemoved. For suggestion click-through, count search_suggestions_clicked against search_opened. See Ask before.
swipeDown, Swipe DownRemoved exit trigger
Stories completed, Moments ContainerValues that don't recase 1:1. See the shared trigger tables.
current_mode, next_modeReplaced by a single viewing_mode string
origin_widget_id, labels_expression, container_idMoved 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 valuesSearch 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.

WasNow
BlazeContainerEventPropsRemoved. BlazeTabEventProps replaces it, and unlike container properties it's exposed on BlazeAnalytics as .tab.
viewing_mode, a PlayerViewingTransitionState object with current_mode and next_modePlayerViewingMode, a single value naming the mode being moved to. The object type is removed.
.playbackPause / .playbackPlay.userInitiatedPlaybackPause / .userInitiatedPlaybackPlay
.adPause / .adResumeRemoved. Ad pause and resume use the shared cases under category ad.
BlazeAnalyticsModels.PlaybackActionMethod, with .press and .releaseRemoved. Anything referencing the type, not just the property, stops compiling.
PlayerExitTriggers.swipeDownRemoved. Swipe to dismiss reports .userSwipeToDismiss, and swiping between playlists reports .swipe.
PlayerExitTriggers and PlayerStartTriggersNew cases: .focusLost on exit, and .focusRegained and .autoAdvance on start. An exhaustive switch over either enum stops compiling until it handles them.
PlayableEndTriggerRemoved. 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_typeRemoved. Those players have a single call to action gesture. BlazeStoryEventProps and BlazeAdEventProps keep it.
BlazeWidgetEventProps.widget_nameRemoved from the SDK and from the tracking plan. On iOS it duplicated widget_id exactly.
BlazeAnalyticsModels.CtaConfig.typeRemoved. It was declared and never populated, and the plan has no such field.
widgetIdentifier, which defaulted to a timestampRequired, and the first argument of every widget initializer and of setup(with:). See the next section.
containerIdentifier, which defaulted to a timestampRequired 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.

FieldWasNow
story_page_typeNever sent from iOSSent on page-scoped Story events, and absent on story_start
video_time_start, video_time_endNever sent from iOSvideo_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_triggerNot reportedReported, and only on back_to_live
moment_exit_triggerReported on a limited set of exit pathsNames the reason on every exit path
ad_exit_triggerSent without a valueReports why every Stories and Moments ad ended
moment.localizationAbsentSent on moment_start, matching Stories and Videos
cc_stateStory and Moment onlyAlso on Video
gesture_type on StoryDeclared, but not populatedPopulated on call to action and custom action button events
thumbnail_format on widget_loadOnly on widget_clickOn both
referring on Interaction eventsAbsentinteraction_view and interaction_answer carry it, so interaction engagement is attributable like every other player event

Values that report differently

What changedWhat it means for your numbers
story_page_navigation_type
  • Timer-driven page advances report automatic, not manual.
  • Expect a large shift toward automatic.
  • The old split between passive and active consumption isn't comparable.
moment_navigation_typeMoment exits report automatic on auto-advance, not manual.
story_page_duration_viewed_percent
  • It's a decimal, and a fully watched page reaches 100. It used to stop at 99.
  • A threshold of = 100 starts working.
  • A threshold of >= 99 now also catches real 99 % views.
story_exit_trigger
  • Completion depends on whether the Story finished, not on whether another follows.
  • Expect completion to rise and skip to fall.
  • skip means the Viewer skipped.
Ad content identity
  • Ads report content_id and content_title on both Videos and Moments.
  • Video ads used to report the video in moment_id and moment_title, and Moment ads reported nothing.
  • Your historical video-ad volume sits with Moment ads.
moment.loop_numberIt increments on every loop.
Casting transitionsSent only when the casting state changes, so expect fewer of them.
cc_on and cc_offOnly a real change reports. Re-selecting the active state, or changing caption language while captions stay on, reports nothing.
Moments seekOne moment.seek for a whole seek bar drag, not one per movement.
Preloaded banner adsThey attribute to the Story the banner was created for, not the Story on screen when the ad network calls back.
Composite feed sources
  • Per-item events name the source the item came from.
  • composite appears only on list-level events, such as widget_load and tab_data_load.
tech device fields
  • Captured once during initialize().
  • An event emitted before that reports unknown.
  • That's at most the first event of a session, and only if you initialize off the main thread.

Behaviors that change event volume

BehaviorWhat it means for your numbers
widget_click fires on every real tap
  • Returning .handledByApp from onWidgetItemClickHandler used to report nothing. The tap itself is now the event.
  • With custom click handling, expect a step change in clicks and click-through rate.
Start and exit events strictly alternate
  • An unpaired event used to appear for the first-time intro slide on the Moments player, an inline Video player restarting in preview, and a pending trigger carried into the next start.
  • Session length, viewed percentage, and completion are more reliable for those views.
app_foreground is a real app resumeIn-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
  • story_start_trigger on later Stories is swipe, skip, or auto_advance, not the playlist entry trigger.
  • A "Stories opened from widget" count that summed story_start rows needs story_index = 0, or unique story_session_id, or it drops.
Description events are capped per Moment view
  • They used to fire on every tap, so five toggles produced ten events.
  • The metric now means Moment views where the description was opened, and raw tap volume isn't recoverable.
  • Rates against moment_start stay valid.
onEventTriggered is exactly-once
  • Startup events could reach your handler twice. Delivery is now once per event, in emission order.
  • If you deduplicated them yourself, that workaround becomes a no-op and can stay.

Step 5: Verify

  1. Confirm api_scheme_version reads 2.1 in the payloads your app receives.
  2. Confirm event_category values arrive lowercase, and that your handler covers tab and global.
  3. 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.
  4. Confirm widgetIdentifier and containerIdentifier are the stable placement labels you intended, and that they're identical across two launches of the app.
  5. 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


Did this page help you?