# Player Deck (iOS)

### **Display Player Deck**

#### Use PlayerDeckView

The `PlayerDeckView` provides a `UIView` wrapper for the `PlayerDeckFeedViewController`.

**Integration**

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

The following are the sample codes:

```swift
import UIKit
import FireworkVideo

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

    func addPlayerDeckView() {
        let channelID = "<Encoded Channel ID>"
        let playlistID = "<Encoded Playlist ID>"
        let playerDeckView = PlayerDeckView(source:
            .channelPlaylist(
                channelID: channelID,
                playlistID: playlistID
            )
        )
        playerDeckView.viewConfiguration = getPlayerDeckContentConfiguration()

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

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

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

#### Use PlayerDeckSwiftUIView(SwiftUI)

The `PlayerDeckSwiftUIView` provides a SwiftUI View wrapper for the `PlayerDeckFeedViewController`.

**Integration**

1. Import `FireworkVideo`.
2. Instantiate `PlayerDeckSwiftUIView` 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 playerDeckContainer = PlayerDeckSwiftUIContainer()
    var body: some View {
        List {
            Spacer()
            PlayerDeckSwiftUIView(
                source: .channelPlaylist(channelID: channelID, playlistID: playlistID),
                viewConfiguration: getPlayerDeckContentConfiguration(),
                isPictureInPictureEnabled: true,
                onPlayerDeckLoaded: {
                    debugPrint("Video feed loaded successfully.")
                },
                onPlayerDeckFailedToLoad: { error in
                    debugPrint("Video feed did fail loading.")
                }
            ).frame(height: 600)
            Button("Refresh") {
                playerDeckContainer.handler?.refresh()
            }
            Spacer()
        }
    }

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

### **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
}
```

### Widget-Level Product Hydration

We support widget-level product hydration in the player deck through the `onProductHydration` closure. This process is triggered immediately after the widget data has finished loading. For more details, please refer to the code snippets below.

```swift
let channelID = "<Encoded Channel ID>"
let playlistID = "<Encoded Playlist ID>"
let playerDeckView = PlayerDeckView(source:
        .channelPlaylist(
            channelID: channelID,
            playlistID: playlistID
        )
)
playerDeckView.onProductHydration = { [weak self] products, videos, hydrator in
    guard let self = self else {
        return
    }
    self.handleWidgetProductHydration(
        products: products,
        videos: videos,
        hydrator: hydrator
    )
}

PlayerDeckSwiftUIView(
    source: .channelPlaylist(channelID: channelID, playlistID: playlistID),
    onProductHydration: { [weak self] products, videos, hydrator in
        guard let self = self else {
            return
        }
        self.handleWidgetProductHydration(
            products: products,
            videos: videos,
            hydrator: hydrator
        )
    }
).frame(height: 600)

func handleWidgetProductHydration(
    products: [Product],
    videos: [VideoDetails],
    hydrator: ProductHydrating
) {
    let productIDs = products.compactMap { $0.externalID }
    // Retrieve the most up-to-date product details
    // based on the product IDs from the host app server
    
    // Introduce a delay to mimic a network request that fetches
    // updated product data after the hydration callback is triggered.
    DispatchQueue.global().asyncAfter(wallDeadline: .now() + 3) {
        // Get product model list
        let productModels = hydrator.products
        // Call hydration API
        for productID in productIDs {
            hydrator.hydrateProduct(productID) { productBuilder in
                // Update product info
                productBuilder
                    .name("Latest product name")
                    .description("Latest product description")
                    .isAvailable(true)

                // Set to true to hide the product, or false to keep it visible
                productBuilder.hidden(true)

                // Update product variants.
                // The strategy can be merge or replace.
                // With merge strategy, we will merge these new variants into existing variants. We use variant id to match the variant.
                // With `replace` strategy, we will replace existing variants with these new variants.
                productBuilder.variants(.merge) { variantsBuilder in
                    // Build variant
                    variantsBuilder.variant("variant id1") { variantBuilder in
                        variantBuilder.formattedPrice(100, currencyCode: "USD")
                            .formattedOriginalPrice(120, currencyCode: "USD")
                            .url("Latest variant url1")
                            .imageUrl("Latest variant image url1")
                            .isAvailable(true)
                            .options([
                                "Color": "Latest variant color1",
                                "Size": "Latest variant size1"
                            ])
                        return variantBuilder
                    }

                    // Build variant
                    variantsBuilder.variant("variant id2") { variantBuilder in
                        variantBuilder.name("Latest variant name2")
                            .formattedPrice(110, currencyCode: "USD")
                            .formattedOriginalPrice(130, currencyCode: "USD")
                            .url("Latest variant url2")
                            .imageUrl("Latest variant image url2")
                            .isAvailable(true)
                            .options([
                                "Color": "Latest variant color2",
                                "Size": "Latest variant size2"
                            ])
                        return variantBuilder
                    }
                    return variantsBuilder
                }
                return productBuilder
            }
        }
    }
}
```

#### Why Is Widget-Level Product Hydration Required?

In the player deck, multiple videos may be displayed at the same time, and each video can have its own associated products.

The existing [global product hydration](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/shopping-ios.md#global-product-hydration) mechanism is limited to handling product data for only one video at a time, which becomes a bottleneck in multi-video scenarios.

Widget-level product hydration removes this limitation by enabling the host app to hydrate product data for multiple videos concurrently, ensuring that products across different videos can be processed simultaneously.

#### Can We Implement Only Widget-Level Product Hydration?

No. In general, [global product hydration](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/shopping-ios.md#global-product-hydration) should be implemented by default. Widget-level hydration is currently supported only in the player deck. If you need to hydrate products for other widgets—such as Video Feed, Circle Story, or Story Block—global product hydration is required.

Widget-level product hydration is designed specifically for multi-video product scenarios. It is triggered immediately after the widget data has finished loading and operates within a limited scope.

It is not intended to replace global product hydration. Instead, it addresses a specific use case within the player deck.

Therefore, widget-level hydration should be considered a complementary mechanism rather than a full replacement for [global product hydration](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/shopping-ios.md#global-product-hydration).

### Force Refresh

A `PlayerDeckView` or `PlayerDeckSwiftUIView` can be forced to refreshed by calling the `refresh()` method on the instance that should be refreshed. This functionality is useful if your feed is embedded along with other components that are also updated and you support features like pull to refresh.

### Receive video feed events

1. Set the delegate

```swift
playerDeckView.delegate = self
```

2. Conform to `PlayerDeckFeedViewControllerDelegate` protocol

```swift
func playerDeckFeedDidLoadFeed(_ viewController: PlayerDeckFeedViewController) {
    print(">>> Player deck feed load successfully")
}

func playerDeckFeed(_ viewController: PlayerDeckFeedViewController, didFailToLoadFeed error: VideoFeedError) {
    print(">>> Player deck feed did fail loading")
    if case .contentSourceError(let playerDeckContentSourceError) = error,
       case .emptyFeed = playerDeckContentSourceError {
        // This is a specific error.
        // SDK will trigger this error when the player deck is empty.
        // For example, host app can hide player deck for this error.
    } else {
        // Other error
    }
}
```

### **Viewport-based autoplay support**

By default, autoplay is enabled and follows a viewport-based behavior: playback starts when the component enters the viewport and pauses when it leaves. This delivers a seamless experience when the component is embedded within a `ScrollView`, `TableView`, or `CollectionView`.

#### 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
```

However, you can further refine this viewport by specifying `safeAreaEdges` and `additionalViewportExcludedInset`. Please refer to the following code snippets for more details.

```swift
/// Viewport Configuration:
/// 
/// With the configuration below, the viewport is calculated as:
/// Viewport = Screen - Top Safe Area - 50pt Bottom Padding
/// 
/// Visual Layout:
/// ┌─────────────────────────┐
/// │ Status Bar              │ \
/// ├─────────────────────────┤  > Excluded (safeAreaEdges = .top)
/// │ Nav Bar (if present)    │ /
/// ╞═════════════════════════╡ ← Viewport Top
/// │                         │
/// │   Viewport Area         │ ← Content visible here
/// │                         │
/// ╞═════════════════════════╡ ← Viewport Bottom
/// │ 50pt bottom padding     │ ← Excluded (additionalViewportExcludedInset.bottom)
/// └─────────────────────────┘
/// 
viewConfiguration.safeAreaEdges = .top
viewConfiguration.additionalViewportExcludedInset = UIEdgeInsets(
    top: 0,
    left: 0,
    bottom: 50,
    right: 0
)
```

### Player Deck configurations

Please refer to [Player Deck Configurations (iOS)](/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/customization-ios/player-deck-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).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.firework.com/firework-for-developers/ios-sdk/integration-guide-for-ios-sdk/player-deck-ios.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
