> 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/storyblock.md).

# Story Block (iOS)

### Display Story Block

#### Use FWSStoryBlockView

The `FWSStoryBlockView` provides a `UIView` wrapper for the `StoryBlockViewController`. You can customize the `FWSStoryBlockView` just like the `StoryBlockViewController`.

**Integration**

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

The following are the sample codes:

<pre class="language-swift"><code class="lang-swift">import UIKit
import FireworkVideo

<strong>class ViewController: UIViewController {
</strong>    override func viewDidLoad() {
        super.viewDidLoad()
        self.addStoryBlockView()
    }

    func addStoryBlockView() {
        let storyBlockView = FWSStoryBlockView(source: .discover)
        // Please ensure that viewConfiguration and isPictureInPictureEnabled are set
        // before attaching it to the parent view
        storyBlockView.viewConfiguration = getStoryBlockConfiguration()
        storyBlockVC.isPictureInPictureEnabled = true

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

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

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
</code></pre>

{% hint style="warning" %}
Presently, it is necessary to configure the story block before it is attached to the parent view, due to a limitation in the current implementation.
{% endhint %}

#### Use FWSStoryBlockSwiftUIView (SwiftUI)

The `FWSStoryBlockSwiftUIView` provides a SwiftUI View wrapper for the `StoryBlockViewController`. You can customize the `FWSStoryBlockSwiftUIView` just like the `StoryBlockViewController`.

**Integration**

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

The following are the sample codes:

```swift
import SwiftUI
import FireworkVideo

struct ContentView: View {
    let storyBlockContainer = FWSStoryBlockSwiftUIContainer()
    var body: some View {
        List {
            Spacer()
            FWSStoryBlockSwiftUIView(
                source: .discover,
                viewConfiguration: getStoryBlockConfiguration(),
                isPictureInPictureEnabled: true,
                onStoryBlockLoaded: {
                    debugPrint("Story block loaded successfully.")
                },
                onStoryBlockFailedToLoad: { error in
                    debugPrint("Story block did fail loading.")
                },
                container: storyBlockContainer
            ).frame(height: 500)
            Button("Play") {
                storyBlockContainer.handler?.play()
            }
            Button("Pause") {
                storyBlockContainer.handler?.pause()
            }
            Spacer()
        }
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

### **Story Block Content Source**

The enum `StoryBlockContentSource` defines the different sources that can be used to populate the story block.

#### **Channel**

Displays content from the specified channel id.

```swift
let channelID = "<Channel ID>"
// UIKit
let storyBlockView = FWSStoryBlockView(source: .channel(channelID: channelID))
// SwiftUI
FWSStoryBlockSwiftUIView(source: .channel(channelID: channelID))
```

#### **Channel Playlist**

Displays content from the specified playlist id.

```swift
let channelID = "<Your Channel ID>"
let playlistID = "<Playlist ID>"
// UIKit
let storyBlockView = FWSStoryBlockView(source: .channelPlaylist(channelID: channelID, playlistID: playlistID))
// SwiftUI
FWSStoryBlockSwiftUIView(source: .channelPlaylist(channelID: channelID, playlistID: playlistID))
```

#### **Dynamic Content**

Displays dynamic content based on the provided channel id and content parameters.

```swift
let channelID = "<Channel ID>"
let parameters: DynamicContentParameters = ["<cohort key>": ["<cohort value 1>", "<cohort value 2>"]]
// UIKit
let storyBlockView = FWSStoryBlockView(source: .dynamicContent(channelID: channelID, parameters: parameters))
// SwiftUI
FWSStoryBlockSwiftUIView(source: .dynamicContent(channelID: channelID, parameters: parameters))
```

#### Hashtag Playlist

Displays content based on the provided channel id and the hashtag expression.

```swift
let channelID = "<Channel ID>"
let singleHashtag = "dogs"
// UIKit
let storyBlockView = FWSStoryBlockView(source: .hashtagPlaylist(channelID: channelID, filterExpression: singleHashtag))
// SwiftUI
FWSStoryBlockSwiftUIView(source: .hashtagPlaylist(channelID: channelID, filterExpression: singleHashtag))
```

Or a more advanced hashtag expression can be used to fine tune the results

```swift
let channelID = "<Channel ID>"
let filterExpression = "(and sport (or food comedy))"
// UIKit
let storyBlockView = FWSStoryBlockView(source: .hashtagPlaylist(channelID: channelID, filterExpression: filterExpression))
// SwiftUI
FWSStoryBlockSwiftUIView(source: .hashtagPlaylist(channelID: channelID, filterExpression: filterExpression))
```

#### SKU Playlist

Displays content based on the provided channel id and SKU/Product external IDs.

```swift
let channelID = "<Encoded Channel ID>"
let productIDs = ["<Product ID>", ...]
let storyBlockView = FWSStoryBlockView(source: .skuPlaylist(channelID: channelID, productIDs: productIDs))
FWSStoryBlockSwiftUIView(source: .skuPlaylist(channelID: channelID, productIDs: productIDs
```

#### Single Video or Live Stream

Displays a single video or live stream content.

```swift
let videoID = "<Video ID>"
// UIKit
let storyBlockView = FWSStoryBlockView(source: .singleContent(contentID: videoID))
// SwiftUI
FWSStoryBlockSwiftUIView(source: .singleContent(contentID: videoID))
```

#### Video Ads

Displays video ads based on vast XML.

```swift
// Specifies which channel the video ads should be created under.
let adChannelId = "<Channel ID>"
// The vast XML.
let vastXml = "<Vast XML>"
// UIKit
let storyBlockView = FWSStoryBlockView(source: .videoAds(adChannelId: adChannelId, vastXml: vastXml))
// SWiftUI
FWSStoryBlockSwiftUIView(source: .videoAds(adChannelId: adChannelId, vastXml: vastXml))
```

#### Channel Videos

```swift
let channelID = "Encoded channel id"
let videoIDs = ["Encoded video id 1", "Encoded video id 2", ...]
// UIKit
let storyBlockView = FWSStoryBlockView(source: .channelVideos(channelID: channelID, videoIDs: videoIDs))
// SwiftUI
FWSStoryBlockView(source: .channelVideos(channelID: channelID, videoIDs: videoIDs))
```

### Receive story block events

1. Set the delegate

```swift
storyBlockVC.delegate = self
```

2. Conform to `StoryBlockViewControllerDelegate` protocol

```swift
func storyBlockDidLoadFeed(
    _ viewController: StoryBlockViewController
) {
    debugPrint("Story block loaded successfully.")
}

func storyBlock(
    _ viewController: StoryBlockViewController,
    didFailToLoadFeed error: StoryBlockError
) {
    debugPrint("Story block did fail loading.")
    if case .contentSourceError(let feedContentSourceError) = error,
       case .emptyFeed = feedContentSourceError {
        // This is a specific error.
        // SDK will trigger this error when the story block is empty.
        // For example, host app can hide story block when receive this error.
    } else {
        // Other error
    }
}
```

### Play and pause StoryBlock programmatically

#### Use FWSStoryBlockView

```swift
let storyBlockView = FWSStoryBlockView(source: .discover)

// Play StoryBlock Programmatically
storyBlockView.play()

// Pause StoryBlock Programmatically
storyBlockView.pause()
```

#### Use FWSStoryBlockSwiftUIView

```swift
import SwiftUI
import FireworkVideo

struct ContentView: View {
    let storyBlockContainer = FWSStoryBlockSwiftUIContainer()
    var body: some View {
        List {
            Spacer()
            FWSStoryBlockSwiftUIView(
                source: .discover,
                container: storyBlockContainer
            ).frame(height: 500)
            Button("Play") {
                storyBlockContainer.handler?.play()
            }
            Button("Pause") {
                storyBlockContainer.handler?.pause()
            }
            Spacer()
        }
    }
}
```

### **Enter Story Block Fullscreen Programmatically**

#### Use FWSStoryBlockView

```swift
let storyBlockView = FWSStoryBlockView(source: .discover)

// Enter story block fullscreen programmatically
storyBlockView.enterFullScreen()
```

#### Use FWSStoryBlockSwiftUIView

```swift
import SwiftUI
import FireworkVideo

struct ContentView: View {
    let storyBlockContainer = FWSStoryBlockSwiftUIContainer()
    var body: some View {
        List {
            Spacer()
            FWSStoryBlockSwiftUIView(
                source: .discover,
                container: storyBlockContainer
            ).frame(height: 500)
            Button("Enter fullscreen") {
                // Enter story block fullscreen programmatically
                storyBlockContainer.handler?.enterFullScreen()
            }
            Spacer()
        }
    }
}
```

### **Autoplay**

Autoplay lets the story block automatically start playing without requiring user interaction. It is controlled by:

```swift
viewConfiguration.autoplay.isEnabled = true // default
```

This is the prerequisite for everything below — it is `true` by default, so autoplay is on out of the box.

#### 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 screen:

* Autoplay starts when the component's visibility within the viewport is greater than or equal to `viewport.threshold`, and pauses when it falls below.
* When the story block is embedded in your own scroll container, the viewport is clipped to that container automatically — a story block scrolled out of it correctly pauses, with no extra configuration.
* The viewport itself is customizable via `viewport.safeAreaEdges` (see below).

```swift
func getStoryBlockConfiguration() -> StoryBlockConfiguration {
    var viewConfiguration = StoryBlockConfiguration()
    viewConfiguration.playbackButton.isHidden = false
    viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false

    // The story block autoplays when 50% or more of its content is visible
    // within the viewport, and pauses when visibility falls below 50%.
    // (0.5 is the default; shown here so you know where to tune it.)
    viewConfiguration.viewport.threshold = 0.5
    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 story block and intersects the bounds of every ancestor view that clips its content, so a story block 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
/// Story block 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.viewport.safeAreaEdges = .top
```

### Player configurations

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

### Story block layout

You can customize the width and height of the story block to achieve either a horizontal or vertical layout.

#### Vertical layout

As shown in the following code snippets, we set the story block’s width to 350 and height to 533 to achieve a horizontal layout.

**Use StoryBlockViewController**

```swift
import FireworkVideo

class ViewController: UIViewController {

    func embedFeedInViewController() {
        let storyBlockVC = StoryBlockViewController(source: .discover)
        // Please ensure that viewConfiguration and isPictureInPictureEnabled are set
        // before attaching it to the parent view
        storyBlockVC.viewConfiguration = getStoryBlockConfiguration()
        storyBlockVC.isPictureInPictureEnabled = true

        self.addChild(storyBlockVC)

        storyBlockVC.view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(storyBlockVC.view)
        let width = 350
        let height = 533
        NSLayoutConstraint.activate([
            storyBlockVC.view.widthAnchor.constraint(equalToConstant: width),
            storyBlockVC.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
            storyBlockVC.view.heightAnchor.constraint(equalToConstant: height),
            storyBlockVC.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])

        storyBlockVC.didMove(toParent: self)
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

**Use StoryBlockView**

```swift
import UIKit
import FireworkVideo
import FireworkVideoUI

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

    func addStoryBlockView() {
        let storyBlockView = StoryBlockView(source: .discover)
        // Please ensure that viewConfiguration and isPictureInPictureEnabled are set
        // before attaching it to the parent view
        storyBlockView.viewConfiguration = getStoryBlockConfiguration()
        storyBlockView.isPictureInPictureEnabled = true

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

        let width = 350
        let height = 533
        NSLayoutConstraint.activate([
            storyBlockView.widthAnchor.constraint(equalToConstant: width),
            storyBlockView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
            storyBlockView.heightAnchor.constraint(equalToConstant: height),
            storyBlockView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

**Use StoryBlockSwiftUIView**

```swift
import SwiftUI
import FireworkVideo
import FireworkVideoUI

struct ContentView: View {
    var body: some View {
        List {
            Spacer()
            StoryBlockSwiftUIView(
                source: .discover,
                viewConfiguration: getStoryBlockConfiguration(),
                isPictureInPictureEnabled: true,
                onStoryBlockLoaded: {
                    debugPrint("Story block loaded successfully.")
                },
                onStoryBlockFailedToLoad: { error in
                    debugPrint("Story block did fail loading.")
                }
            ).frame(width: 350, height: 533)
            Spacer()
        }
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

<figure><img src="https://688917408-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MLoGG8m6bokS9YTmS7m%2Fuploads%2Fgit-blob-e30d935f0989475cc467c6884dcb58d0a0662935%2Fimage%20(3).png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Horizontal layout

As shown in the following code snippets, we set the story block’s width to 533 and height to 255 to achieve a horizontal layout.

**Use StoryBlockViewController**

```swift
import FireworkVideo

class ViewController: UIViewController {

    func embedFeedInViewController() {
        let storyBlockVC = StoryBlockViewController(source: .discover)
        // Please ensure that viewConfiguration and isPictureInPictureEnabled are set
        // before attaching it to the parent view
        storyBlockVC.viewConfiguration = getStoryBlockConfiguration()
        storyBlockVC.isPictureInPictureEnabled = true

        self.addChild(storyBlockVC)

        storyBlockVC.view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(storyBlockVC.view)
        let width = 5330
        let height = 255
        NSLayoutConstraint.activate([
            storyBlockVC.view.widthAnchor.constraint(equalToConstant: width),
            storyBlockVC.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
            storyBlockVC.view.heightAnchor.constraint(equalToConstant: height),
            storyBlockVC.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])

        storyBlockVC.didMove(toParent: self)
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

**Use StoryBlockView**

```swift
import UIKit
import FireworkVideo
import FireworkVideoUI

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

    func addStoryBlockView() {
        let storyBlockView = StoryBlockView(source: .discover)
        // Please ensure that viewConfiguration and isPictureInPictureEnabled are set
        // before attaching it to the parent view
        storyBlockView.viewConfiguration = getStoryBlockConfiguration()
        storyBlockView.isPictureInPictureEnabled = true

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

        let width = 533
        let height = 255
        NSLayoutConstraint.activate([
            storyBlockView.widthAnchor.constraint(equalToConstant: width),
            storyBlockView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
            storyBlockView.heightAnchor.constraint(equalToConstant: height),
            storyBlockView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

**Use StoryBlockSwiftUIView**

```swift
import SwiftUI
import FireworkVideo
import FireworkVideoUI

struct ContentView: View {
    var body: some View {
        List {
            Spacer()
            StoryBlockSwiftUIView(
                source: .discover,
                viewConfiguration: getStoryBlockConfiguration(),
                isPictureInPictureEnabled: true,
                onStoryBlockLoaded: {
                    debugPrint("Story block loaded successfully.")
                },
                onStoryBlockFailedToLoad: { error in
                    debugPrint("Story block did fail loading.")
                }
            ).frame(width: 350, height: 533)
            Spacer()
        }
    }

    func getStoryBlockConfiguration() -> StoryBlockConfiguration {
        var viewConfiguration = StoryBlockConfiguration()
        viewConfiguration.playbackButton.isHidden = false
        viewConfiguration.fullScreenPlayerView.playbackButton.isHidden = false
        return viewConfiguration
    }
}
```

<figure><img src="https://688917408-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MLoGG8m6bokS9YTmS7m%2Fuploads%2Fgit-blob-8884512786529ffc9ee8eda85734c9a4df3e441e%2Fimage%20(4).png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

### Loading indicator color

You can use the following code snippets to customize loading indicator color.

```
var configuration = StoryBlockConfiguration()
// Specifies the color of the loading indicator shown when content is loading
configuration.loadingIndicatorColor = .gray

let storyBlockVC = StoryBlockViewController(source: .discover)
storyBlockVC.viewConfiguration = configuration

let storyBlockView = StoryBlockView(source: .discover)
storyBlockView.viewConfiguration = configuration

StoryBlockSwiftUIView(
    source: .discover,
    viewConfiguration: configuration,
)
```
