> For the complete documentation index, see [llms.txt](https://docs.firework.com/firework-for-developers/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.firework.com/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/video-feed.md).

# Video Feed (iOS)

### **Display Video Feed**

#### Use FWSVideoFeedView

The `FWSVideoFeedView` provides a `UIView` wrapper for the `VideoFeedViewController`. You can customize the `FWSVideoFeedView` just like the `VideoFeedViewController`.

**Integration**

1. Import `FireworkVideo`.
2. Instantiate `FWSVideoFeedView` and embed it.

The following are the sample codes:

```swift
import UIKit
import FireworkVideo

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.addVideoFeedView()
    }

    func addVideoFeedView() {
        let channelID = "<Encoded Channel ID>"
        let playlistID = "<Encoded Playlist ID>"
        let videoFeedView = FWSVideoFeedView(source: .channelPlaylist(channelID: channelID, playlistID: playlistID))
        videoFeedView.viewConfiguration = getVideoFeedContentConfiguration()

        videoFeedView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(videoFeedView)

        NSLayoutConstraint.activate([
            videoFeedView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            videoFeedView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            videoFeedView.heightAnchor.constraint(equalToConstant: 240),
            videoFeedView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
    }

    func getVideoFeedContentConfiguration() -> VideoFeedContentConfiguration {
        var viewConfiguration = VideoFeedContentConfiguration()
        viewConfiguration.itemView.autoplay.isEnabled = true
        viewConfiguration.playerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

#### Use FWSVideoFeedSwiftUIView(SwiftUI)

The `FWSVideoFeedSwiftUIView` provides a SwiftUI View wrapper for the `VideoFeedViewController`. You can customize the `FWSVideoFeedSwiftUIView` just like the `VideoFeedViewController`.

**Integration**

1. Import `FireworkVideo`.
2. Instantiate `FWSVideoFeedSwiftUIView` and embed it.

The following are the sample codes:

```swift
import SwiftUI
import FireworkVideo

let channelID = "<Encoded Channel ID>"
let playlistID = "<Encoded Playlist ID>"

struct ContentView: View {
    let videoFeedContainer = FWSVideoFeedSwiftUIContainer()
    var body: some View {
        List {
            Spacer()
            FWSVideoFeedSwiftUIView(
                source: .channelPlaylist(channelID: channelID, playlistID: playlistID),
                viewConfiguration: getVideoFeedContentConfiguration(),
                isPictureInPictureEnabled: true,
                onVideoFeedLoaded: {
                    debugPrint("Video feed loaded successfully.")
                },
                onVideoFeedFailedToLoad: { error in
                    debugPrint("Video feed did fail loading.")
                }
            ).frame(height: 240)
            Button("Refresh") {
                videoFeedContainer.handler?.refresh()
            }
            Spacer()
        }
    }

    func getVideoFeedContentConfiguration() -> VideoFeedContentConfiguration {
        var viewConfiguration = VideoFeedContentConfiguration()
        viewConfiguration.itemView.autoplay.isEnabled = true
        viewConfiguration.playerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

### **Autosizing (grid layout)**

By default the feed fills the frame you give it and scrolls its own content. Autosizing inverts that: with a grid layout (`VideoFeedGridLayout`), the feed grows to the full height of its content, reports that height as its intrinsic content size, and lets **your** scroll view do the scrolling. That is what you want when the feed is one block inside a longer page.

Pass `autosizingEnabled: true` when you create the view.

```swift
func addAutosizingVideoFeed(to stackView: UIStackView) {
    let channelID = "<Encoded Channel ID>"
    let playlistID = "<Encoded Playlist ID>"
    let videoFeedView = FWSVideoFeedView(
        layout: VideoFeedGridLayout(),
        source: .channelPlaylist(channelID: channelID, playlistID: playlistID),
        autosizingEnabled: true
    )
    videoFeedView.viewConfiguration = getVideoFeedContentConfiguration()

    // No height constraint — the feed supplies its own intrinsic height.
    videoFeedView.translatesAutoresizingMaskIntoConstraints = false
    stackView.addArrangedSubview(videoFeedView)
}
```

In SwiftUI, place it in your `ScrollView` without a `.frame(height:)`.

```swift
ScrollView {
    VStack(spacing: 20) {
        MyPageHeader()
        FWSVideoFeedSwiftUIView(
            layout: VideoFeedGridLayout(),
            source: .channelPlaylist(channelID: channelID, playlistID: playlistID),
            viewConfiguration: getVideoFeedContentConfiguration(),
            autosizingEnabled: true
        )
        MyPageFooter()
    }
}
```

#### What autosizing changes

* **The feed stops scrolling internally** and reports its content height as its intrinsic content size. Do not give it a fixed height constraint or `.frame(height:)`.
* **At most 20 videos are rendered** — one page. A grid grows taller with every page loaded, so it has to stay bounded. Set `fetchConfiguration.maxVideos` to show fewer; a value above 20 is clamped back to 20. This cap applies only to an autosized grid — without autosizing, or with a row layout, `maxVideos` is not limited.

{% hint style="warning" %}
`autosizingEnabled` and the kind of layout — grid or row — are fixed when the view is created. Assigning a layout of the other kind afterwards is ignored, and trips an assertion in debug builds. To switch, replace the view with a freshly initialized one; in SwiftUI, give it a new `.id()`.
{% endhint %}

### **Content Source**

Please refer to [Video Feed Content Source (iOS)](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/video-feed-content-source-ios.md).

### **Custom Call-To-Action Button Handling**

Custom Call-To-Action button handling is done via the `FireworkVideoCTADelegate` protocol. This provides control over what occurs when a call-to-action button is tapped.

1. Set the delegate:

```swift
FireworkVideoSDK.ctaDelegate = self
```

2\. Conform to protocol:

```swift
func handleCustomCTAClick(_ viewController: PlayerViewController, url: URL, for video: VideoDetails) -> Bool {
    // Your custom action code here...
    return true
}
```

### Force Refresh

You can force a `VideoFeedViewController` to reload its content by calling the `refresh()` method on the instance you want to update. This is useful when your feed is embedded alongside other components that refresh together, or when you support features like pull-to-refresh.

### Receive video feed events

1. Set the delegate

```swift
feedVC.delegate = self
```

2. Conform to `VideoFeedViewControllerDelegate` protocol

```swift
func videoFeedDidLoadFeed(
    _ viewController: VideoFeedViewController
) {
    debugPrint("Video feed loaded successfully.")
}

func videoFeed(
    _ viewController: VideoFeedViewController,
    didFailToLoadFeed error: VideoFeedError
) {
    debugPrint("Video feed did fail loading.")
    if case .contentSourceError(let feedContentSourceError) = error,
       case .emptyFeed = feedContentSourceError {
        // This is a specific error.
        // SDK will trigger this error when the feed is empty.
        // For example, host app can hide video feed for this error.
    } else {
        // Other error
    }
}
```

### Receive onVideosLoaded callback

The `onVideosLoaded` closure is called whenever videos are loaded into the video feed, including the initial page and every subsequent page loaded via pagination. The closure receives a `VideoFeedVideosLoadedInfo` value whose `videos` property contains the `[VideoDetails]` that were loaded for that page.

#### Use FWSVideoFeedView

`onVideosLoaded` is exposed as a property on `FWSVideoFeedView`:

```swift
let videoFeedView = FWSVideoFeedView(source: .channelPlaylist(channelID: channelID, playlistID: playlistID))
videoFeedView.onVideosLoaded = { info in
    debugPrint("Loaded \(info.videos.count) videos.")
    for video in info.videos {
        debugPrint("Video ID: \(video.id)")
    }
}
```

#### Use FWSVideoFeedSwiftUIView(SwiftUI)

`onVideosLoaded` is exposed as an initializer parameter on `FWSVideoFeedSwiftUIView`:

```swift
FWSVideoFeedSwiftUIView(
    source: .channelPlaylist(channelID: channelID, playlistID: playlistID),
    viewConfiguration: getVideoFeedContentConfiguration(),
    isPictureInPictureEnabled: true,
    onVideosLoaded: { info in
        debugPrint("Loaded \(info.videos.count) videos.")
        for video in info.videos {
            debugPrint("Video ID: \(video.id)")
        }
    }
).frame(height: 240)
```

### **Autoplay**

Autoplay lets the video feed automatically start playing the first eligible item without requiring user interaction. To enable it, set:

```swift
viewConfiguration.itemView.autoplay.isEnabled = true
```

All behavior described below assumes this prerequisite is met. Which item is picked as "eligible" depends on the visibility threshold (`viewConfiguration.itemView.autoplay.triggerVisibilityPercentage`).

#### Viewport-based autoplay (default)

Autoplay is viewport-based by default, which delivers a seamless experience when the component is embedded in a `ScrollView`, `TableView`, or `CollectionView`. Visibility is measured against the viewport rather than the component's own bounds:

* The first item whose visibility **within the viewport** is greater than or equal to `triggerVisibilityPercentage` starts playing automatically.
* When the feed is embedded in your own scroll container, the viewport is clipped to that container automatically — a feed scrolled out of it correctly pauses, with no extra configuration.
* The viewport itself is customizable via `safeAreaEdges` (see below).

```swift
func getVideoFeedContentConfiguration() -> VideoFeedContentConfiguration {
    var viewConfiguration = VideoFeedContentConfiguration()
    viewConfiguration.itemView.autoplay.isEnabled = true
    viewConfiguration.playerView.playbackButton.isHidden = false
    return viewConfiguration
}
```

#### Customize viewport

The default viewport is defined as the screen bounds minus the safe area insets—such as the status bar, top navigation bar, bottom tab bar, and bottom home indicator.

```swift
/// Default Viewport = Screen - Top Safe Area - Bottom Safe Area
/// 
/// Screen (Full Device Screen)
/// ┌─────────────────────────┐ ← Screen Top
/// │   Status Bar            │ ← Safe Area (excluded)
/// ├─────────────────────────┤
/// │   Navigation Bar        │ ← Safe Area (excluded, if present)
/// ├─────────────────────────┤
/// │                         │
/// │                         │
/// │   Default Viewport      │ ← Visible content area
/// │   (Visible Content)     │    (Screen - Safe Area)
/// │                         │
/// │                         │
/// ├─────────────────────────┤
/// │   Tab Bar               │ ← Safe Area (excluded, if present)
/// ├─────────────────────────┤
/// │   Home Indicator        │ ← Safe Area (excluded)
/// └─────────────────────────┘ ← Screen Bottom
```

The viewport is then clipped to whatever your own layout hides. The SDK walks up from the feed and intersects the bounds of every ancestor view that clips its content, so a feed scrolled out of your scroll container — or hidden behind a header stacked above that container — is measured as not visible. This is automatic and needs no configuration.

```swift
/// Feed embedded in your own scroll view:
///
/// ┌─ Screen ────────────────────────┐
/// │ Status Bar / Nav Bar            │ ← Safe Area (excluded)
/// ├─────────────────────────────────┤
/// │ Your page header                │ ← Excluded: outside the scroll
/// ╞═ Your scroll view (clips) ══════╡    view, which clips it away
/// │                                 │
/// │   Viewport                      │ ← What's left
/// │                                 │
/// ╞═════════════════════════════════╡
/// │ Rest of your page               │ ← Excluded, same as above
/// ├─────────────────────────────────┤
/// │ Home Indicator                  │ ← Safe Area (excluded)
/// └─────────────────────────────────┘
```

If you want only some safe-area edges excluded, narrow them with `safeAreaEdges`.

```swift
/// With the configuration below, only the top safe area is excluded:
/// Viewport = Screen - Top Safe Area
///
/// ┌─────────────────────────────────┐
/// │ Status Bar                      │ \
/// ├─────────────────────────────────┤  > Excluded (safeAreaEdges = .top)
/// │ Nav Bar (if present)            │ /
/// ╞═════════════════════════════════╡ ← Viewport Top
/// │                                 │
/// │   Viewport Area                 │ ← Content visible here
/// │                                 │
/// │                                 │
/// └─────────────────────────────────┘ ← Viewport Bottom (bottom safe area kept)
///
viewConfiguration.safeAreaEdges = .top
```

### Video feed configurations

Please refer to [Video feed configurations (iOS)](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/customization-ios/video-feed-configurations-ios.md).

### Player configurations

Please refer to [Player configurations (iOS)](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/customization-ios/player-configurations-ios.md).
