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
- Persistence & sync
- Integration
- Customization
- Data models
- Best practices
- Example: Complete integration
- Troubleshooting
- Support
Architecture
The Follow Entities system consists of several key components:
BlazeFollowEntitiesManager: The main instance for managing follow stateBlazeFollowEntity: Model representing a followable entity, carrying anid, an optionalproviderName, and an optionalentityTypeBlazeFollowedEntityCategory: 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 toproviderNameandBlazeFollowedEntityCategory)BlazeMomentsPlayerFollowEntityStyle: Style configuration for the follow UI componentBlazeFollowEntitiesDelegate: 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.followEntitiesManagerSetting 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
)
setFollowedEntitiesreplaces 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. PreferinsertFollowedEntities/removeFollowedEntities(see Managing follow state) for anything additive; reach forsetFollowedEntitiesonly 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
newFollowingStatereflects 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 = trueEntity 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 = .firstAvailableVisual 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 labelContent 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
.textcontent source will automatically be treated as.iconto 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 = .starThe 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:
iconSourcehas 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 —providerNameandentityTypeare carried metadata and aren't part of equality or hashing.providerNameis 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 inonFollowEntityClickedandgetFollowedEntities(). Host-seededset/insert/removecalls can supply them explicitly. Passing an entity withoutproviderNameorentityTypekeeps 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:
BlazeFollowedEntityCategorydescribes what an entity is.BlazeFollowEntityTypeis 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 = truein 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.
Updated about 9 hours ago
