GuidesAPI ReferenceRelease Notes
HomeLog InHome
Guides

Follow entities - iOS

The Follow entities feature allows viewers to follow and unfollow entities (players, teams, properties/competitions) directly within the Blaze SDK player experience. Follow state is persisted on-device automatically, and — once activity sync is enabled and the viewer is identified — synced across the viewer's devices through Blaze. This feature provides a seamless way to track viewer preferences without your app needing to own the storage.

Table of contents

Architecture

The Follow Entities system consists of several key components:

  • BlazeFollowEntitiesManager: The main instance for managing follow state
  • BlazeFollowEntity: Model representing a followable entity, carrying an id, an optional providerName, and an optional entityType
  • BlazeFollowedEntityCategory: Enum describing what a followed entity is (.player, .team, .property)
  • BlazeFollowEntityType: Enum defining entity types with fallback support (used to pick which entity a Moment's follow button shows — unrelated to providerName and BlazeFollowedEntityCategory)
  • BlazeMomentsPlayerFollowEntityStyle: Style configuration for the follow UI component
  • BlazeFollowEntitiesDelegate: Delegate for handling user interactions

Persistence & sync

Out of the box

Every follow/unfollow — from a tap on the follow button, or from your app calling BlazeFollowEntitiesManager directly — is written to a local store on the device immediately. This happens for every viewer, including anonymous ones, survives app restarts, and works fully offline. No backend and no extra setup are required for this baseline behavior.

Syncing across devices

If your app has activity sync enabled (allowUserActivityRemoteSync) and the viewer is identified (externalUserId set), those local changes also sync to Blaze and back — the same gate and pipeline used for the SDK's other user-activity signals. There's no separate follow-specific flag.

Sync is not real-time: it rides the SDK's normal periodic batch/refresh cycle, not an immediate push on every tap. A follow made on one device typically appears on another after the next sync cycle on each side, not instantly.

📘

Your app is no longer required to persist follow state itself or re-seed the SDK on every launch. The sections below (setting initial state, insertFollowedEntities/removeFollowedEntities) are for apps that want to seed from their own backend, mirror state in their own UI, or manage follow state independent of Blaze's sync.

Integration

Basic setup

Access the follow entities manager through the Blaze SDK singleton:

import BlazeSDK

let followEntitiesManager = Blaze.shared.followEntitiesManager

Setting initial follow state

In most integrations you don't need this at all — the SDK already has the viewer's follow state persisted locally (and synced, if enabled) from previous sessions. Use this only if you want to seed or override state from your own backend, for example on first login for a viewer the SDK hasn't seen before:

// Create follow entities from your app's data.
// providerName is optional — an opaque string naming the data provider the entity
// came from (e.g. "stats-perform"); forwarded as-is, not validated by the SDK.
// entityType is optional — the category of the entity (.player, .team, .property).
let followedEntities: Set<BlazeFollowEntity> = [
    BlazeFollowEntity(id: "player_123", providerName: "stats-perform", entityType: .player),
    BlazeFollowEntity(id: "team_456", entityType: .team),
    BlazeFollowEntity(id: "league_789", entityType: .property)
]

// Get base player style
var momentsPlayerStyle = BlazeMomentsPlayerStyle.base()

// Enable follow entity component (by default it's not visible)
momentsPlayerStyle.followEntity.isVisible = true

// Set the followed entities (replacing any existing entities — see the warning below)
followEntitiesManager.setFollowedEntities(followedEntities)

// Run player
Blaze.shared.playMoments(
    dataSourceType: .yourDataSource,
    style: momentsPlayerStyle,
    triggerSource: .entryPoint
)
❗️

setFollowedEntities replaces all existing entities — locally and, once synced, for the viewer across every device, latest-wins. It will silently erase a follow made moments earlier on another device if that entity isn't included in the set you pass. Prefer insertFollowedEntities/removeFollowedEntities (see Managing follow state) for anything additive; reach for setFollowedEntities only when you deliberately mean "this is the complete, correct list."

Important: If you do call this before presenting the player, do it before opening the player so the UI reflects the correct state from the start.

Handling user interactions

Implement the BlazeFollowEntitiesDelegate to receive callbacks when users interact with follow buttons:

class YourViewController: UIViewController, BlazeFollowEntitiesDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set delegate
        Blaze.shared.followEntitiesManager.delegate = self
    }
    
    func onFollowEntityClicked(_ params: BlazeFollowEntityClickedParams) {
        let entityId = params.followEntity.id
        let isNowFollowing = params.newFollowingState
        let contentId = params.sourceId
        let playerType = params.playerType
        
        if isNowFollowing {
            // User just followed the entity
            print("User followed entity: \(entityId) from content: \(contentId)")
            // Sync with your backend
            yourBackendService.followEntity(entityId)
        } else {
            // User just unfollowed the entity
            print("User unfollowed entity: \(entityId) from content: \(contentId)")
            // Sync with your backend
            yourBackendService.unfollowEntity(entityId)
        }
    }
}

Note: The internal follow state is automatically updated before the delegate is called, so newFollowingState reflects the state after the user's action.

Managing follow state — incremental vs. replace

Two different kinds of operation exist, and they behave very differently once cross-device sync is involved:

  • Incremental (insertFollowedEntities / removeFollowedEntities) — additive, safe to call from any device without affecting anything else. Use these for anything short of a full resync.
  • Replace (setFollowedEntities, see above) — authoritative full-list override, "latest wins" across devices. Use only when you deliberately mean "this is the complete, correct list."

Adding followed entities

Add new entities to the existing followed set without replacing all entities:

let newEntities: Set<BlazeFollowEntity> = [
    BlazeFollowEntity(id: "player_999")
]

followEntitiesManager.insertFollowedEntities(newEntities)

Removing followed entities

Remove specific entities from the followed set:

let entitiesToRemove: Set<BlazeFollowEntity> = [
    BlazeFollowEntity(id: "player_123")
]

followEntitiesManager.removeFollowedEntities(entitiesToRemove)

Getting current follow state

Retrieve all currently followed entities:

let currentlyFollowed = followEntitiesManager.getFollowedEntities()

for entity in currentlyFollowed {
    print("Entity ID: \(entity.id)")
}

Customization

Player style configuration

Enable or disable the follow entity UI component via the player style:

// Get base player style
var momentsPlayerStyle = BlazeMomentsPlayerStyle.base()

// Enable follow entity component
momentsPlayerStyle.followEntity.isVisible = true

Entity type priority

The BlazeFollowEntityType enum provides a powerful mechanism for entity type resolution with fallback chains. When content has multiple followable entities (e.g., a goal moment might have both a player and a team), the SDK uses this type to determine which entity to display in the UI.

How fallback works:

The SDK attempts to find an entity matching your preferred type. If not found, it follows the fallback chain until a match is found or the chain ends.

Usage examples:

// Get followEntity from the base player style
var followEntity = BlazeMomentsPlayerStyle.base().followEntity

// Example 1: Player only (no fallback)
// Will show player entity, or hide component if no player available
followEntity.entityType = .player(fallbackType: nil)

// Example 2: Player with team fallback
// Will show player if available, otherwise team, otherwise hide component
followEntity.entityType = .player(fallbackType: .team(fallbackType: nil))

// Example 3: Complex fallback chain
// Priority: Player → Team → Property → Any available entity
followEntity.entityType = .player(
    fallbackType: .team(
        fallbackType: .property(
            fallbackType: .firstAvailable
        )
    )
)

// Example 4: Team with "any" fallback
// Will show team if available, otherwise any available entity type
followEntity.entityType = .team(fallbackType: .firstAvailable)

// Example 5: Accept any available entity (no preference)
// Will show the first entity returned from backend
followEntity.entityType = .firstAvailable

Visual customization

Customize the appearance of the follow button for both follow and unfollow states:

var momentsPlayerStyle = BlazeMomentsPlayerStyle.base()

// Customize the FOLLOWED state
momentsPlayerStyle.followEntity.followState.avatar.borderWidth = 2.0
momentsPlayerStyle.followEntity.followState.avatar.borderColor = .blue
momentsPlayerStyle.followEntity.followState.chip.backgroundColor = .green
momentsPlayerStyle.followEntity.followState.chip.iconColor = .black

// Customize the UNFOLLOWED state
momentsPlayerStyle.followEntity.unfollowState.avatar.borderWidth = 1.5
momentsPlayerStyle.followEntity.unfollowState.avatar.borderColor = .white
momentsPlayerStyle.followEntity.unfollowState.chip.backgroundColor = .white
momentsPlayerStyle.followEntity.unfollowState.chip.iconColor = .black
momentsPlayerStyle.followEntity.unfollowState.chip.contentSource = .text // Show "Follow" text label

Content source options

The chip can display different content based on your design requirements:

  • .icon — Displays an icon (compact representation)
  • .text — Displays "Follow" text (only effective in unfollowed state)

Note: When in the followed state, the .text content source will automatically be treated as .icon to maintain consistent visual representation.

Icon source options

While the chip shows an icon, iconSource picks the built-in icon. Each state has its own setting:

  • .default: a + while unfollowed and a checkmark while followed.
  • .star: an outlined star while unfollowed and a filled star while followed.

To use the star, set it on both states:

momentsPlayerStyle.followEntity.unfollowState.chip.iconSource = .star
momentsPlayerStyle.followEntity.followState.chip.iconSource = .star

The star takes the chip's iconColor. Because the two states are set separately, changing only one of them mixes icons, for example a + before following and a filled star after.

Note: iconSource has no effect while the unfollowed chip shows text (contentSource = .text).

Data models

BlazeFollowEntity

Represents a followable entity with a unique identifier, an optional data-provider name, and an optional category.

public struct BlazeFollowEntity: Equatable, Hashable {
    public let id: String
    public let providerName: String?
    public let entityType: BlazeFollowedEntityCategory?

    public init(id: String, providerName: String? = nil, entityType: BlazeFollowedEntityCategory? = nil)
}

Note: Identity is id-only — providerName and entityType are carried metadata and aren't part of equality or hashing. providerName is an opaque, unvalidated string naming the data provider that sourced the entity (e.g. "stats-perform"). Both are forwarded to User Activity when the signal syncs. Content-driven follow taps (from the Moments player) populate them automatically when the source content has them, and they're reported in onFollowEntityClicked and getFollowedEntities(). Host-seeded set/insert/remove calls can supply them explicitly. Passing an entity without providerName or entityType keeps any value the SDK already knows for that id.

BlazeFollowedEntityCategory

The category of a followed entity. Use it to tell the SDK what kind of entity a BlazeFollowEntity is, and to restrict the followed entities data source to some categories.

public enum BlazeFollowedEntityCategory: Hashable, Sendable {
    case player
    case team
    case property   // e.g. a competition or a league
}

Note: BlazeFollowedEntityCategory describes what an entity is. BlazeFollowEntityType is a style setting that picks which entity the follow button displays, with fallbacks.

BlazeFollowEntityType

Defines the type of entity with hierarchical fallback support.

public enum BlazeFollowEntityType: Equatable, Hashable {
    case firstAvailable
    case player(fallbackType: BlazeFollowEntityType?)
    case team(fallbackType: BlazeFollowEntityType?)
    case property(fallbackType: BlazeFollowEntityType?)
}

BlazeFollowEntityClickedParams

Encapsulates information about a follow entity click event.

public struct BlazeFollowEntityClickedParams {
    public let sourceId: String                    // Content ID where click occurred
    public let playerType: BlazePlayerType         // Player type (.moments, .stories, .videos)
    public let newFollowingState: Bool             // State after the click
    public let followEntity: BlazeFollowEntity     // The clicked entity
}

BlazeMomentsPlayerFollowEntityStyle

Complete style configuration for the follow entity component.

public struct BlazeMomentsPlayerFollowEntityStyle {
    public var isVisible: Bool
    public var followState: BlazeMomentsPlayerFollowEntityStateStyle
    public var unfollowState: BlazeMomentsPlayerFollowEntityStateStyle
    public var entityType: BlazeFollowEntityType
}

BlazeMomentsPlayerFollowEntityChipStyle

Style of the follow chip in one state, available as followState.chip and unfollowState.chip.

public struct BlazeMomentsPlayerFollowEntityChipStyle {
    public var backgroundColor: UIColor
    public var iconColor: UIColor                                        // Tint of the icon or text
    public var contentSource: BlazeMomentsPlayerFollowEntityChipContentSource
    public var iconSource: BlazeMomentsPlayerFollowEntityIconSource
}

public enum BlazeMomentsPlayerFollowEntityChipContentSource {
    case icon
    case text     // Unfollowed state only; the followed state always shows an icon
}

public enum BlazeMomentsPlayerFollowEntityIconSource {
    case `default` // `+` while unfollowed, checkmark while followed
    case star      // Outlined star while unfollowed, filled star while followed
}

Best practices

1. Let the SDK own persistence and sync

You generally don't need to call setFollowedEntities on every launch anymore — the SDK already restores the viewer's follow state from its local store (and from Blaze, if activity sync is enabled) before you'd ever get a chance to seed it. Reserve setFollowedEntities for cases where your own backend is the source of truth you want to seed from or override with — and if you do call it, do so before opening the player:

// ✅ Good — if you need to seed/override from your own data
followEntitiesManager.setFollowedEntities(yourFollowedEntities)
Blaze.shared.playMoments(...)

// ❌ Bad - UI might show incorrect state initially
Blaze.shared.playMoments(...)
followEntitiesManager.setFollowedEntities(yourFollowedEntities) // Too late!

2. Only add your own persistence if you need the signal elsewhere

The delegate callback is no longer required for follow state to survive or sync — the SDK already persists and syncs it. Use the callback only if you need the event for something outside the SDK (analytics, mirroring state in your own UI, your own backend for other systems):

func onFollowEntityClicked(_ params: BlazeFollowEntityClickedParams) {
    // Optional: only if you need this signal outside the SDK
    logFollowEvent(entityId: params.followEntity.id, isFollowing: params.newFollowingState)
}

3. Don't try to revert SDK state on your own backend's failure

If you maintain a parallel backend for your own purposes, don't call insertFollowedEntities/removeFollowedEntities to "revert" the SDK's state when your backend call fails — the SDK's own state and sync to Blaze are independent of your backend and are already correct. Treat your backend integration and the SDK's built-in persistence as two separate concerns; don't fight one with the other.

4. Use entity type priority wisely

Configure entity type priority based on your content:

// For sports content focused on individual athletes
momentsPlayerStyle.followEntity.entityType = .player(
    fallbackType: .team(fallbackType: nil)
)

// For team sports content
momentsPlayerStyle.followEntity.entityType = .team(
    fallbackType: .player(fallbackType: nil)
)

// For league/competition content
momentsPlayerStyle.followEntity.entityType = .property(
    fallbackType: .firstAvailable
)

5. Clean up delegate

Remove the delegate reference when your instance that used it is deallocated:

deinit {
    Blaze.shared.followEntitiesManager.delegate = nil
}

Example: Complete integration

Minimal integration (recommended baseline)

Follow state persists and syncs on its own — most integrations need nothing more than enabling the UI component:

import UIKit
import BlazeSDK

class YourMomentsViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        openPlayer()
    }

    private func openPlayer() {
        var style = BlazeMomentsPlayerStylePresets.base()

        // Enable follow entity
        style.followEntity.isVisible = true
        style.followEntity.entityType = .player(
            fallbackType: .team(fallbackType: .firstAvailable)
        )

        // Customize colors
        style.followEntity.followState.chip.backgroundColor = UIColor(hexString: "#FF00B27C")!
        style.followEntity.unfollowState.chip.contentSource = .text

        // Play moments — no follow-state seeding needed; the SDK already
        // restored it from its local store (and from Blaze, if synced).
        Blaze.shared.playMoments(
            dataSourceType: .yourDataSource,
            entryContentId: "moment_123",
            style: style,
            triggerSource: .entryPoint
        )
    }
}

Advanced: mirroring the signal in your own backend

Only needed if you have a reason to keep your own copy of follow state outside Blaze (for example, a separate feature that isn't backed by the SDK). This is additive to the SDK's own persistence and sync — don't use it to "correct" the SDK's state:

import UIKit
import BlazeSDK

class YourMomentsViewController: UIViewController {

    private let backendService = YourBackendService()
    private var followManager: BlazeFollowEntitiesManager {
        Blaze.shared.followEntitiesManager
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        followManager.delegate = self
        openPlayer()
    }

    deinit {
        followManager.delegate = nil
    }

    private func openPlayer() {
        var style = BlazeMomentsPlayerStylePresets.base()
        style.followEntity.isVisible = true
        style.followEntity.entityType = .player(
            fallbackType: .team(fallbackType: .firstAvailable)
        )

        Blaze.shared.playMoments(
            dataSourceType: .yourDataSource,
            entryContentId: "moment_123",
            style: style,
            triggerSource: .entryPoint
        )
    }
}

// MARK: - BlazeFollowEntitiesDelegate

extension YourMomentsViewController: BlazeFollowEntitiesDelegate {

    func onFollowEntityClicked(_ params: BlazeFollowEntityClickedParams) {
        let entityId = params.followEntity.id
        let isFollowing = params.newFollowingState

        // Log analytics
        logFollowEvent(
            entityId: entityId,
            action: isFollowing ? "follow" : "unfollow",
            sourceId: params.sourceId,
            playerType: params.playerType
        )

        // Mirror to your own backend, for your own purposes.
        // Do NOT call insertFollowedEntities/removeFollowedEntities here to
        // "revert" on failure — the SDK's own state and sync to Blaze are
        // independent of this call and are already correct.
        Task {
            do {
                if isFollowing {
                    try await backendService.followEntity(entityId)
                } else {
                    try await backendService.unfollowEntity(entityId)
                }
            } catch {
                logFollowMirrorFailure(entityId: entityId, error: error)
            }
        }
    }
}

Troubleshooting

Follow button not appearing

  • Ensure followEntity.isVisible = true in your player style
  • Verify that the content has follow entities in the backend response
  • Check that the entity type priority matches available entities

Wrong follow state displayed

  • Set initial follow state before opening the player
  • Verify entity IDs match between your system and backend

Delegate not being called

  • Ensure delegate is set before opening the player
  • Verify delegate is not nil
  • Check that the delegate conforms to BlazeFollowEntitiesDelegate

Support

For additional support or questions about the Follow entity feature, please contact the Blaze SDK team or refer to the main SDK documentation.


Did this page help you?