Storyteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title, subtitle, and button. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user.
Cards currently support 1:1, 2:3, 3:4, 4:5, 9:16, 16:9, and 4:1 aspect ratios from CMS payloads.
For SwiftUI, use the StorytellerCard view component.
Configuration: Create a StorytellerCardConfiguration object, specifying the collectionId for the Card collection you want to display. You can also provide optional context data for analytics attribution. When configured, context will be included in all analytics events when users interact with the Card. See Analytics for more details.
Model: Initialize a StorytellerCardModel (which conforms to ObservableObject) with the configuration.
View: Create the StorytellerCard view, passing in the StorytellerCardModel instance.
Actions (Optional): Provide an optional callback closure to the StorytellerCard initializer to handle events like onDataLoadComplete. This allows you to react to data loading success or failure (e.g., by hiding the component).
importSwiftUIimportStorytellerSDK@available(iOS14.0,*)structSwiftUIView:View{@StateObjectprivatevarcardModel=StorytellerCardModel(configuration:StorytellerCardConfiguration(collectionId:"card-collection-id",context:["source":"hero-banner"]))varbody:someView{VStack{Text("Storyteller Card Section")StorytellerCard(model:cardModel){actioninswitchaction{case.onDataLoadComplete(letresult):switchresult{case.success:print("Card data loaded successfully!")case.failure(leterror):print("Card data failed to load: \(error.localizedDescription)")}@unknowndefault:break}}Button("Reload Card"){cardModel.reload()}}.padding()}}
For UIKit, use the StorytellerCardView class, which subclasses UIView.
Configuration: Create a StorytellerCardConfiguration with the desired collectionId. You can also provide optional context data for analytics. When configured, context will be included in all analytics events when users interact with the Card. See Analytics for more details.
View Initialization: Instantiate StorytellerCardView using the configuration.
Delegate (Optional): Assign an object conforming to StorytellerCardViewDelegate to the view's delegate property to receive callbacks like onDataLoadComplete.
Add to View Hierarchy: Add the StorytellerCardView instance as a subview.
classCardView:UIView,StorytellerCardViewDelegate{privatevarstorytellerCardView:StorytellerCardView?// ...funcconfigure(withcollectionId:String,delegate:StorytellerCardViewDelegate?){letconfiguration=StorytellerCardConfiguration(collectionId:collectionId,context:["source":"hero-banner",])letcardView=StorytellerCardView(configuration:configuration)cardView.delegate=delegateaddSubview(cardView)// Add constraintsself.storytellerCardView=cardView}funcreloadCard(){storytellerCardView?.reload()}nonisolatedfunconDataLoadComplete(result:Result<Void,Error>){// Respond to the load result as needed.}}
For UIKit integration patterns using table view cells, see the CocoaPods sample in MultipleListsDataSource.
We also provide cells to include inside collection and table views - StorytellerCardCollectionViewCell and StorytellerCardTableViewCell. They also follow the above pattern to setup.
Both StorytellerCardModel (for SwiftUI) and the UIKit flavours provide a reload() method. Call this method to manually trigger a refresh of the Card data from the server.
In the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content.
Video Cards start from the Cards audio behavior in tenant settings. For the usual muted-toggle setup, the active video Card shows a mute/unmute control and starts muted. The selected Cards audio state is shared across the current Cards surface, so if a user unmutes one video Card, newly active video Cards stay unmuted until the user mutes again or an audio interruption occurs.
Only the active video Card can be audible. Inactive video Cards stay muted, and image Cards do not show an audio control. If the API marks a video Card as having no audio, the active Card does not show an audio control and remains muted.
Cards audio behavior is controlled by the CMS/API settings theme under theme.behavior.cards:
showMuteToggle: set true to show the toggle for active audio-capable video Cards. False, nil, or missing keeps Cards muted with no icon.
persistMuteState: set true to persist the user's Cards mute choice. False, nil, or missing keeps Cards state session-scoped.
defaultMuteState: uses soundOff, soundOn, or respectDeviceSilentToggle. Missing, nil, or unrecognized values default to soundOff for Cards.
Card appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection.
The following properties can be configured in the CMS and influence the Card's presentation and behaviour:
Button positioning: Buttons are optional visual elements that follow the textOverContent property:
When textOverContent = true: Button appears on the card (overlaying the content), positioned below the title/subtitle
When textOverContent = false: Button appears below the card (below the title/subtitle section)
Button functionality: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped
Button text: The button text is defined in the Card data, not the theme
Button content inset: Buttons apply a built-in horizontal content inset when the button text is edge-aligned (start or end), using the resolved style.padding value so the label does not sit flush against the border.
style.textLengthMode (default: truncate): How text that exceeds the available space is handled.
truncate: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit.
resize: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading).
style.textAlignment (default: start): Horizontal alignment of the heading and subheading. Can be start, center, or end.
style.padding (default: 12): Inner padding around the text content. For full-bleed cards ( marginHorizontal = 0) with text below the image and all cards with text on the image, padding is applied to all sides of the text. For cards with text below the image where marginHorizontal > 0, padding is applied only to the top and bottom of the text.
style.marginHorizontal (default: 0): Horizontal margin around the card. 0 means full-bleed.
style.cornerRadius (default: {theme.primitives.cornerRadius}): Corner radius of the card. The application depends on marginHorizontal and text position. Not applied for full-bleed cards (marginHorizontal=0) with text below the image. Applied to the image for cards with text below the image and marginHorizontal > 0. Applied to the whole card for cards with text on the image and marginHorizontal > 0.
style.headingsSpacing (default: 3): Vertical spacing between the heading and subheading.
style.buttonSpacing (default: 12): Vertical spacing before the button when a card button is present. In the common title/subtitle case, this is the spacing between the headings block and the button.
style.dynamicTypeEnabled (default: true): Whether cards typography participates in Dynamic Type scaling. When false, heading, subheading, and button text use fixed font sizes and fixed line-height behavior.
style.backgroundColorLight (optional): Light-mode background color for the text container when textOverContent = false and the card is full-bleed (marginHorizontal = 0).
style.backgroundColorDark (optional): Dark-mode background color for the text container when textOverContent = false and the card is full-bleed (marginHorizontal = 0).
style.heading.font (default: {theme.customFont}): Font family for the heading.
style.heading.textSize (default: 22): Font size for the heading.
style.heading.lineHeight (default: nil): Line height for the heading. If not specified, the font's default line height is used.
style.heading.textCase (default: default): Text case transformation (upper, lower, default).
style.heading.letterSpacing (default: 0): Letter spacing for the heading.
style.heading.textColor (default: {theme.colors.white.primary}): Text color for the heading when text is displayed on the background asset.
style.heading.textBelowContentColorLight (optional): Light-mode heading text color override when textOverContent = false.
style.heading.textBelowContentColorDark (optional): Dark-mode heading text color override when textOverContent = false.
style.subHeading.font (default: {theme.customFont}): Font family for the subheading.
style.subHeading.textSize (default: 16): Font size for the subheading.
style.subHeading.lineHeight (default: nil): Line height for the subheading. If not specified, the font's default line height is used.
style.subHeading.textCase (default: default): Text case transformation (upper, lower, default).
style.subHeading.letterSpacing (default: 0): Letter spacing for the subheading.
style.subHeading.textColor (default: {theme.colors.white.secondary}): Text color for the subheading when text is displayed on the background asset.
style.subHeading.textBelowContentColorLight (optional): Light-mode subheading text color override when textOverContent = false.
style.subHeading.textBelowContentColorDark (optional): Dark-mode subheading text color override when textOverContent = false.
style.button.title.font (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height.
style.button.title.textSize (default: 16): Font size for the button text.
style.button.title.lineHeight (default: 21): Line height for the button text.
style.button.title.textCase (default: default): Text case transformation for the button text (upper, lower, default).
style.button.title.letterSpacing (default: 0): Letter spacing for the button text.
style.button.title.textColor (default: {theme.colors.white.primary}): Text color for the button when text is displayed on the background asset.
style.button.title.textBelowContentColorLight (optional): Light-mode button text color override when textOverContent = false.
style.button.title.textBelowContentColorDark (optional): Dark-mode button text color override when textOverContent = false.
style.button.backgroundColor (optional): Background color of the button. If not set, the button will have a transparent background with an outline.
style.button.outlineColor (default: {theme.colors.white.primary} for text on image, resolved style.button.title.textBelowContentColorLight / style.button.title.textBelowContentColorDark for text below image when provided, otherwise {theme.colors.black.primary} in light mode and {theme.colors.white.primary} in dark mode): Color of the button outline/border. If not specified or null, it follows the theme defaults for text-over-content cards and matches the resolved button title color for text-below-content cards.
style.button.outlineWidth (default: 1): Width of the button outline/border in points.
style.button.cornerRadius (optional, default: {theme.primitives.cornerRadius}): Corner radius of the button. If null or not set, falls back to the theme's default corner radius.
style.button.textAlignment (default: uses card textAlignment): Text alignment for the button text. If not specified or null, uses the card's text alignment setting.
behavior.reloading.reloadOnExit (default: true): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player).
behavior.reloading.reloadOnForeground (default: true): Whether the Card Collection reloads when the app comes to the foreground.
{"slug": "cards", "page_title": "Add Storyteller Cards", "page_url": "Cards/", "canonical_url": "/ios/Cards/", "markdown": "# Storyteller Cards\n\nStoryteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title, subtitle, and button. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user.\n\nCards currently support `1:1`, `2:3`, `3:4`, `4:5`, `9:16`, `16:9`, and `4:1` aspect ratios from CMS payloads.\n\n## Usage\n\nYou can integrate Storyteller Cards into your app using either SwiftUI or UIKit.\n\n### SwiftUI\n\nFor SwiftUI, use the `StorytellerCard` view component.\n\n1. **Configuration**: Create a `StorytellerCardConfiguration` object, specifying the `collectionId` for the Card collection you want to display. You can also provide optional `context` data for analytics attribution. When configured, `context` will be included in all analytics events when users interact with the Card. See [Analytics](Analytics.md#context) for more details.\n1. **Model**: Initialize a `StorytellerCardModel` (which conforms to `ObservableObject`) with the configuration.\n1. **View**: Create the `StorytellerCard` view, passing in the `StorytellerCardModel` instance.\n1. **Actions (Optional)**: Provide an optional callback closure to the `StorytellerCard` initializer to handle events like `onDataLoadComplete`. This allows you to react to data loading success or failure (e.g., by hiding the component).\n\n<!-- storyteller-swift-example: id=cards-01 target=sdk-ios context=declarations -->\n\n```swift\nimport SwiftUI\nimport StorytellerSDK\n\n@available(iOS 14.0, *)\nstruct SwiftUIView: View {\n @StateObject private var cardModel = StorytellerCardModel(\n configuration: StorytellerCardConfiguration(\n collectionId: \"card-collection-id\",\n context: [\"source\": \"hero-banner\"]\n )\n )\n\n var body: some View {\n VStack {\n Text(\"Storyteller Card Section\")\n StorytellerCard(model: cardModel) { action in\n switch action {\n case .onDataLoadComplete(let result):\n switch result {\n case .success:\n print(\"Card data loaded successfully!\")\n case .failure(let error):\n print(\"Card data failed to load: \\(error.localizedDescription)\")\n }\n @unknown default:\n break\n }\n }\n\n Button(\"Reload Card\") {\n cardModel.reload()\n }\n }\n .padding()\n }\n}\n```\n\nSee the Showcase [`StorytellerCard`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L41) integration in [`StorytellerItemView`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L10).\n\nSee the Showcase card callback handling in [`StorytellerItemView.cardAction`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/main/ShowcaseApp/Views/Home/Components/StorytellerItemView.swift#L95).\n\n### UIKit\n\nFor UIKit, use the `StorytellerCardView` class, which subclasses `UIView`.\n\n1. **Configuration**: Create a `StorytellerCardConfiguration` with the desired `collectionId`. You can also provide optional `context` data for analytics. When configured, `context` will be included in all analytics events when users interact with the Card. See [Analytics](Analytics.md#context) for more details.\n1. **View Initialization**: Instantiate `StorytellerCardView` using the configuration.\n1. **Delegate (Optional)**: Assign an object conforming to `StorytellerCardViewDelegate` to the view's `delegate` property to receive callbacks like `onDataLoadComplete`.\n1. **Add to View Hierarchy**: Add the `StorytellerCardView` instance as a subview.\n\n<!-- storyteller-swift-example: id=cards-02 target=sdk-ios context=declarations -->\n\n```swift\nclass CardView: UIView, StorytellerCardViewDelegate {\n\n private var storytellerCardView: StorytellerCardView?\n\n // ...\n\n func configure(with collectionId: String, delegate: StorytellerCardViewDelegate?) {\n let configuration = StorytellerCardConfiguration(\n collectionId: collectionId,\n context: [\n \"source\": \"hero-banner\",\n ]\n )\n let cardView = StorytellerCardView(configuration: configuration)\n cardView.delegate = delegate\n\n addSubview(cardView)\n // Add constraints\n\n self.storytellerCardView = cardView\n }\n\n func reloadCard() {\n storytellerCardView?.reload()\n }\n\n nonisolated func onDataLoadComplete(result: Result<Void, Error>) {\n // Respond to the load result as needed.\n }\n}\n```\n\nFor UIKit integration patterns using table view cells, see the CocoaPods sample in [`MultipleListsDataSource`](https://github.com/getstoryteller/storyteller-showcase-ios/blob/11.6.1/cocoapods/StorytellerSampleApp/Components/MultipleLists/TableView/MultipleListsDataSource.swift#L17).\n\nWe also provide cells to include inside collection and table views - `StorytellerCardCollectionViewCell` and `StorytellerCardTableViewCell`. They also follow the above pattern to setup.\n\n## Reloading\n\nBoth `StorytellerCardModel` (for SwiftUI) and the UIKit flavours provide a `reload()` method. Call this method to manually trigger a refresh of the Card data from the server.\n\n## Viewed/Tapped Ordering\n\nIn the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content.\n\n## Video Card Audio\n\nVideo Cards start from the Cards audio behavior in tenant settings. For the usual muted-toggle setup, the active video Card shows a mute/unmute control and starts muted. The selected Cards audio state is shared across the current Cards surface, so if a user unmutes one video Card, newly active video Cards stay unmuted until the user mutes again or an audio interruption occurs.\n\nOnly the active video Card can be audible. Inactive video Cards stay muted, and image Cards do not show an audio control. If the API marks a video Card as having no audio, the active Card does not show an audio control and remains muted.\n\nCards audio behavior is controlled by the CMS/API settings theme under `theme.behavior.cards`:\n\n- `showMuteToggle`: set `true` to show the toggle for active audio-capable video Cards. False, nil, or missing keeps Cards muted with no icon.\n- `persistMuteState`: set `true` to persist the user's Cards mute choice. False, nil, or missing keeps Cards state session-scoped.\n- `defaultMuteState`: uses `soundOff`, `soundOn`, or `respectDeviceSilentToggle`. Missing, nil, or unrecognized values default to `soundOff` for Cards.\n\n## Theming\n\nCard appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection.\nThe following properties can be configured in the CMS and influence the Card's presentation and behaviour:\n\n### Button Behavior\n\n* **Button positioning**: Buttons are optional visual elements that follow the `textOverContent` property:\n* When `textOverContent = true`: Button appears on the card (overlaying the content), positioned below the title/subtitle\n* When `textOverContent = false`: Button appears below the card (below the title/subtitle section)\n* **Button functionality**: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped\n* **Button text**: The button text is defined in the Card data, not the theme\n* **Button content inset**: Buttons apply a built-in horizontal content inset when the button text is edge-aligned (`start` or `end`), using the resolved `style.padding` value so the label does not sit flush against the border.\n\n* `style.textLengthMode` (default: `truncate`): How text that exceeds the available space is handled.\n* `truncate`: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit.\n* `resize`: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading).\n* `style.textAlignment` (default: `start`): Horizontal alignment of the heading and subheading. Can be `start`, `center`, or `end`.\n* `style.padding` (default: `12`): Inner padding around the text content. For full-bleed cards ( `marginHorizontal = 0`) with text *below* the image and *all* cards with text *on* the image, padding is applied to all sides of the text. For cards with text *below* the image where `marginHorizontal > 0`, padding is applied only to the top and bottom of the text.\n* `style.marginHorizontal` (default: `0`): Horizontal margin around the card. `0` means full-bleed.\n* `style.cornerRadius` (default: `{theme.primitives.cornerRadius}`): Corner radius of the card. The application depends on `marginHorizontal` and text position. Not applied for full-bleed cards (`marginHorizontal=0`) with text *below* the image. Applied to the *image* for cards with text *below* the image and `marginHorizontal > 0`. Applied to the *whole card* for cards with text *on* the image and `marginHorizontal > 0`.\n* `style.headingsSpacing` (default: `3`): Vertical spacing between the heading and subheading.\n* `style.buttonSpacing` (default: `12`): Vertical spacing before the button when a card button is present. In the common title/subtitle case, this is the spacing between the headings block and the button.\n* `style.dynamicTypeEnabled` (default: `true`): Whether cards typography participates in Dynamic Type scaling. When `false`, heading, subheading, and button text use fixed font sizes and fixed line-height behavior.\n* `style.backgroundColorLight` (optional): Light-mode background color for the text container when `textOverContent = false` and the card is full-bleed (`marginHorizontal = 0`).\n* `style.backgroundColorDark` (optional): Dark-mode background color for the text container when `textOverContent = false` and the card is full-bleed (`marginHorizontal = 0`).\n* `style.heading.font` (default: `{theme.customFont}`): Font family for the heading.\n* `style.heading.textSize` (default: `22`): Font size for the heading.\n* `style.heading.lineHeight` (default: `nil`): Line height for the heading. If not specified, the font's default line height is used.\n* `style.heading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`).\n* `style.heading.letterSpacing` (default: `0`): Letter spacing for the heading.\n* `style.heading.textColor` (default: `{theme.colors.white.primary}`): Text color for the heading when text is displayed *on* the background asset.\n* `style.heading.textBelowContentColorLight` (optional): Light-mode heading text color override when `textOverContent = false`.\n* `style.heading.textBelowContentColorDark` (optional): Dark-mode heading text color override when `textOverContent = false`.\n* `style.subHeading.font` (default: `{theme.customFont}`): Font family for the subheading.\n* `style.subHeading.textSize` (default: `16`): Font size for the subheading.\n* `style.subHeading.lineHeight` (default: `nil`): Line height for the subheading. If not specified, the font's default line height is used.\n* `style.subHeading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`).\n* `style.subHeading.letterSpacing` (default: `0`): Letter spacing for the subheading.\n* `style.subHeading.textColor` (default: `{theme.colors.white.secondary}`): Text color for the subheading when text is displayed *on* the background asset.\n* `style.subHeading.textBelowContentColorLight` (optional): Light-mode subheading text color override when `textOverContent = false`.\n* `style.subHeading.textBelowContentColorDark` (optional): Dark-mode subheading text color override when `textOverContent = false`.\n\n### Button Theme Properties\n\n* `style.button.title.font` (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height.\n* `style.button.title.textSize` (default: `16`): Font size for the button text.\n* `style.button.title.lineHeight` (default: `21`): Line height for the button text.\n* `style.button.title.textCase` (default: `default`): Text case transformation for the button text (`upper`, `lower`, `default`).\n* `style.button.title.letterSpacing` (default: `0`): Letter spacing for the button text.\n* `style.button.title.textColor` (default: `{theme.colors.white.primary}`): Text color for the button when text is displayed *on* the background asset.\n* `style.button.title.textBelowContentColorLight` (optional): Light-mode button text color override when `textOverContent = false`.\n* `style.button.title.textBelowContentColorDark` (optional): Dark-mode button text color override when `textOverContent = false`.\n* `style.button.backgroundColor` (optional): Background color of the button. If not set, the button will have a transparent background with an outline.\n* `style.button.outlineColor` (default: `{theme.colors.white.primary}` for text on image, resolved `style.button.title.textBelowContentColorLight` / `style.button.title.textBelowContentColorDark` for text below image when provided, otherwise `{theme.colors.black.primary}` in light mode and `{theme.colors.white.primary}` in dark mode): Color of the button outline/border. If not specified or null, it follows the theme defaults for text-over-content cards and matches the resolved button title color for text-below-content cards.\n* `style.button.outlineWidth` (default: `1`): Width of the button outline/border in points.\n* `style.button.cornerRadius` (optional, default: `{theme.primitives.cornerRadius}`): Corner radius of the button. If null or not set, falls back to the theme's default corner radius.\n* `style.button.textAlignment` (default: uses card `textAlignment`): Text alignment for the button text. If not specified or null, uses the card's text alignment setting.\n\n### Behavior Properties\n\n* `behavior.reloading.reloadOnExit` (default: `true`): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player).\n* `behavior.reloading.reloadOnForeground` (default: `true`): Whether the Card Collection reloads when the app comes to the foreground.\n", "copy_markdown_include_header": false, "base_path": "", "ai_dir": "ai", "missing_payload_behavior": "empty"}