Content Card implementation guide

This optional and advanced implementation guide covers Content Card code considerations, three custom use cases built by our team, accompanying code snippets, and guidance on logging impressions, clicks, and dismissals. Visit our Braze Demo Repository here! Please note that this implementation guide is centered around a Swift implementation, but Objective-C snippets are provided for those interested.

Code considerations

Content Cards as custom objects

Much like a rocketship adding a booster, your own custom objects can be extended to function as Content Cards. Limited API surfaces such as this provide flexibility to work with different data backends interchangeably. This can be done by conforming to the ContentCardable protocol and implementing the initializer (as seen below) and, through the use of the ContentCardData struct, allows you to access the ABKContentCard data. The ABKContentCard payload will be used to initialize the ContentCardData struct and the custom object itself, all from a Dictionary type via the initializer the protocol comes with.

The initializer also includes a ContentCardClassType enum. This enum is used to decide which object to initialize. Through the use of key-value pairs within the Braze dashboard, you can set an explicit class_type key that will be used to determine what object to initialize. These key-value pairs for Content Cards come through in the extras variable on the ABKContentCard. Another core component of the initializer is the metaData dictionary parameter. The metaData includes everything from the ABKContentCard parsed out into a series of keys and values. Once the relevant cards are parsed and converted to your custom objects, the app is ready to begin working with them as if they were instantiated from JSON or any other source.

Once you have a solid understanding of these code considerations, check out our use cases below to get started implementing your custom objects.

ContentCardable Protocol
A ContentCardData object that represents the ABKContentCard data along with a ContentCardClassType enum. An initializer used to instantiate custom objects with ABKContentCard metadata.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol ContentCardable {
  var contentCardData: ContentCardData? { get }
  init?(metaData: [ContentCardKey: Any], classType contentCardClassType: ContentCardClassType)
}
 
extension ContentCardable {
  var isContentCard: Bool {
    return contentCardData != nil
  }
   
  func logContentCardClicked() {
    BrazeManager.shared.logContentCardClicked(idString: contentCardData?.contentCardId)
  }
   
  func logContentCardDismissed() {
    BrazeManager.shared.logContentCardDismissed(idString: contentCardData?.contentCardId)
  }
   
  func logContentCardImpression() {
    BrazeManager.shared.logContentCardImpression(idString: contentCardData?.contentCardId)
  }
}

Content Card Data Struct
ContentCardData represents the parsed out values of an ABKContentCard.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentCardData: Hashable {
  let contentCardId: String
  let contentCardClassType: ContentCardClassType
  let createdAt: Double
  let isDismissable: Bool
  ...
  // other Content Card properties such as expiresAt, pinned, etc.
}
 
extension ContentCardData: Equatable {
  static func ==(lhs: ContentCardData, rhs: ContentCardData) -> Bool {
    return lhs.contentCardId == rhs.contentCardId
  }
}

ContentCardable Protocol
A ContentCardData object that represents the ABKContentCard data along with a ContentCardClassType enum, an initializer used to instantiate custom objects with ABKContentCard metadata.

1
2
3
4
5
6
7
8
9
10
11
12
@protocol ContentCardable <NSObject>
 
@property (nonatomic, strong) ContentCardData *contentCardData;
- (instancetype __nullable)initWithMetaData:(NSDictionary *)metaData
                                  classType:(enum ContentCardClassType)classType;
 
- (BOOL)isContentCard;
- (void)logContentCardImpression;
- (void)logContentCardClicked;
- (void)logContentCardDismissed;
 
@end

Content Card Data Struct
ContentCardData represents the parsed out values of an ABKContentCard.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface ContentCardData : NSObject
 
+ (ContentCardClassType)contentCardClassTypeForString:(NSString *)rawValue;
 
- (instancetype)initWithIdString:(NSString *)idString
                       classType:(ContentCardClassType)classType
                       createdAt:(double)createdAt isDismissible:(BOOL)isDismissible;
 
@property (nonatomic, readonly) NSString *contentCardId;
@property (nonatomic) ContentCardClassType classType;
@property (nonatomic, readonly) double *createdAt;
@property (nonatomic, readonly) BOOL isDismissible;
...
// other Content Card properties such as expiresAt, pinned, etc.    
 
@end

Custom Object Initializer
MetaData from an ABKContentCard is used to populate your object’s variables. The key-value pairs set up in the Braze dashboard are represented in the “extras” dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension CustomObject: ContentCardable {
  init?(metaData: [ContentCardKey: Any], classType contentCardClassType: ContentCardClassType) {
    guard let idString = metaData[.idString] as? String,
      let createdAt = metaData[.created] as? Double,
      let isDismissable = metaData[.dismissable] as? Bool,
      let extras = metaData[.extras] as? [AnyHashable: Any],
      else { return nil }
 
    let contentCardData = ContentCardData(contentCardId: idString, contentCardClassType: contentCardClassType, createdAt: createdAt, isDismissable: isDismissable)
    let customObjectProperty = extras["YOUR-CUSTOM-OBJECT-PROPERTY"] as? String
           
    self.init(contentCardData: contentCardData, property: customObjectProperty)
  }
}

Identifying Types
The ContentCardClassType enum represents the class_type value in the Braze dashboard. This value is also used as a filter identifier to display Content Cards in different places.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum ContentCardClassType: Hashable {
  case yourValue
  case yourOtherValue
  ...
  case none
 
  init(rawType: String?) {
    switch rawType?.lowercased() {
    case "your_value": // these values much match the value set in the Braze dashboard
      self = .yourValue
    case "your_other_value": // these values much match the value set in the Braze dashboard
      self = .yourOtherValue
    ...
    default:
      self = .none
    }
  }
}

Custom Object Initializer
MetaData from an ABKContentCard is used to populate your object’s variables. The key-value pairs set up in the Braze dashboard are represented in the “extras” dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (id _Nullable)initWithMetaData:(nonnull NSDictionary *)metaData classType:(enum ContentCardClassType)classType {
  self = [super init];
  if (self) {
    if ([metaData objectForKey:ContentCardKeyIdString] && [metaData objectForKey:ContentCardKeyCreated] && [metaData objectForKey:ContentCardKeyDismissible] && [metaData objectForKey:ContentCardKeyExtras]) {
      NSDictionary  *extras = metaData[ContentCardKeyExtras];
      NSString *idString = metaData[ContentCardKeyIdString];
      double createdAt = [metaData[ContentCardKeyCreated] doubleValue];
      BOOL isDismissible = metaData[ContentCardKeyDismissible];
 
      if ([extras objectForKey: @"YOUR-CUSTOM-PROPERTY")
        _customObjectProperty = extras[@"YOUR-CUSTOM-OBJECT-PROPERTY"];
 
      self.contentCardData = [[ContentCardData alloc] initWithIdString:idString classType:classType createdAt:createdAt isDismissible:isDismissible];
 
      return self;
    }
  }
  return nil;
}

Identifying Types
The ContentCardClassType enum represents the class_type value in the Braze dashboard. This value is also used as a filter identifier to display Content Cards in different places.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef NS_ENUM(NSInteger, ContentCardClassType) {
  ContentCardClassTypeNone = 0,
  ContentCardClassTypeYourValue,
  ContentCardClassTypeYourOtherValue,
  ...
};
 
+ (NSArray *)contentCardClassTypeArray {
  return @[ @"", @"your_value", @"your_other_value" ];
}
 
+ (ContentCardClassType)contentCardClassTypeForString:(NSString*)rawValue {
  if ([[self contentCardClassTypeArray] indexOfObject:rawValue] == NSNotFound) {
    return ContentCardClassTypeNone;
  } else {
    NSInteger value = [[self contentCardClassTypeArray] indexOfObject:rawValue];
    return (ContentCardClassType) value;
  }
}

Requesting Content Cards
As long as the observer is still retained in memory, the notification callback from the Braze SDK can be expected.

1
2
3
4
func loadContentCards() {
  BrazeManager.shared.addObserverForContentCards(observer: self, selector: #selector(contentCardsUpdated))
  BrazeManager.shared.requestContentCardsRefresh()
}

Handling the Content Cards SDK Callback
Forward the notification callback to the helper file to parse the payload data for your custom object(s).

1
2
3
4
5
@objc func contentCardsUpdated(_ notification: Notification) {
  guard let contentCards = BrazeManager.shared.handleContentCardsUpdated(notification, for: [.yourValue]) as? [CustomObject],!contentCards.isEmpty else { return }
 
 // do something with your array of custom objects
}

Working with Content Cards
The class_type is passed in as a filter to only return Content Cards that have a matching class_type.

1
2
3
4
5
func handleContentCardsUpdated(_ notification: Notification, for classTypes: [ContentCardClassType]) -> [ContentCardable] {
  guard let updateIsSuccessful = notification.userInfo?[ABKContentCardsProcessedIsSuccessfulKey] as? Bool, updateIsSuccessful, let cards = contentCards else { return [] }
             
  return convertContentCards(cards, for: classTypes)
}

Requesting Content Cards
As long as the observer is still retained in memory, the notification callback from the Braze SDK can be expected.

1
2
3
4
- (void)loadContentCards {
  [[BrazeManager shared] addObserverForContentCards:self selector:@selector(contentCardsUpdated:)];
  [[BrazeManager shared] requestContentCardsRefresh];
}

Handling the Content Cards SDK Callback
Forward the notification callback to the helper file to parse the payload data for your custom object(s).

1
2
3
4
5
6
- (void)contentCardsUpdated:(NSNotification *)notification {
  NSArray *classTypes = @[@(ContentCardClassTypeYourValue)];
  NSArray *contentCards = [[BrazeManager shared] handleContentCardsUpdated:notification forClassTypes:classTypes];
 
  // do something with your array of custom objects
}

Working with Content Cards
The class_type is passed in as a filter to only return Content Cards that have a matching class_type.

1
2
3
4
5
6
7
8
- (NSArray *)handleContentCardsUpdated:(NSNotification *)notification forClassType:(ContentCardClassType)classType {  
  BOOL updateIsSuccessful = [notification.userInfo[ABKContentCardsProcessedIsSuccessfulKey] boolValue];
  if (updateIsSuccessful) {
    return [self convertContentCards:self.contentCards forClassType:classType];
  } else {
    return @[];
  }
}

Working with Payload Data
Loops through the array of Content Cards and only parses the cards with a matching class_type. The payload from an ABKContentCard is parsed into a Dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func convertContentCards(_ cards: [ABKContentCard], for classTypes: [ContentCardClassType]) -> [ContentCardable] {
  var contentCardables: [ContentCardable] = []
    
  for card in cards {
    let classTypeString = card.extras?[ContentCardKey.classType.rawValue] as? String
    let classType = ContentCardClassType(rawType: classTypeString)
    guard classTypes.contains(classType) else { continue }
       
    var metaData: [ContentCardKey: Any] = [:]
    switch card {
    case let banner as ABKBannerContentCard:
      metaData[.image] = banner.image
    case let captioned as ABKCaptionedImageContentCard:
      metaData[.title] = captioned.title
      metaData[.cardDescription] = captioned.cardDescription
      metaData[.image] = captioned.image
    case let classic as ABKClassicContentCard:
      metaData[.title] = classic.title
      metaData[.cardDescription] = classic.cardDescription
    default:
      break
    }
 
    metaData[.idString] = card.idString
    metaData[.created] = card.created
    metaData[.dismissible] = card.dismissible
    metaData[.urlString] = card.urlString
    metaData[.extras] = card.extras
    ...
    // other Content Card properties such as expiresAt, pinned, etc.
      
    if let contentCardable = contentCardable(with: metaData, for: classType) {
      contentCardables.append(contentCardable)
    }
  }
  return contentCardables
}

Initializing your Custom Objects from Content Card Payload Data
The class_type is used to determine which of your custom objects will be initialized from the payload data.

1
2
3
4
5
6
7
8
9
10
11
func contentCardable(with metaData: [ContentCardKey: Any], for classType: ContentCardClassType) -> ContentCardable? {
  switch classType {
  case .yourValue:
    return CustomObject(metaData: metaData, classType: classType)
  case .yourOtherValue:
    return OtherCustomObject(metaData: metaData, classType: classType)
  ...
  default:
    return nil
  }
}

Working with Payload Data
Loops through the array of Content Cards and only parses the cards with a matching class_type. The payload from an ABKContentCard is parsed into a Dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (NSArray *)convertContentCards:(NSArray<ABKContentCard*> *)cards forClassType:(ContentCardClassType)classType {
  NSMutableArray *contentCardables = [[NSMutableArray alloc] init];      for (ABKContentCard *card in cards) {
    NSString *classTypeString = [card.extras objectForKey:ContentCardKeyClassType];
    ContentCardClassType cardClassType = [ContentCardData contentCardClassTypeForString: classTypeString];
    if (cardClassType != classType) { continue; }
     
    NSMutableDictionary *metaData = [[NSMutableDictionary alloc] init];
    if ([card isKindOfClass:[ABKBannerContentCard class]]) {
      ABKBannerContentCard *banner = (ABKBannerContentCard *)card;
      metaData[ContentCardKeyImage] = banner.image;
    } else if ([card isKindOfClass:[ABKCaptionedImageContentCard class]]) {
      ABKCaptionedImageContentCard *captioned = (ABKCaptionedImageContentCard *)card;
      metaData[ContentCardKeyTitle] = captioned.title;
      metaData[ContentCardKeyCardDescription] = captioned.cardDescription;
      metaData[ContentCardKeyImage] = captioned.image;
    } else if ([card isKindOfClass:[ABKClassicContentCard class]]) {
      ABKClassicContentCard *classic = (ABKClassicContentCard *)card;
      metaData[ContentCardKeyCardDescription] = classic.title;
      metaData[ContentCardKeyImage] = classic.image;
    }
     
    metaData[ContentCardKeyIdString] = card.idString;
    metaData[ContentCardKeyCreated] = [NSNumber numberWithDouble:card.created];
    metaData[ContentCardKeyDismissible] = [NSNumber numberWithBool:card.dismissible];
    metaData[ContentCardKeyUrlString] = card.urlString;
    metaData[ContentCardKeyExtras] = card.extras;
    ...
    // other Content Card properties such as expiresAt, pinned, etc.   
 
    id<ContentCardable> contentCardable = [self contentCardableWithMetaData:metaData forClassType:classType];
    if (contentCardable) {
      [contentCardables addObject:contentCardable];
    }
  }
 
  return contentCardables;
}

Initializing your Custom Objects from Content Card Payload Data
The class_type is used to determine which of your custom objects will be initialized from the payload data.

1
2
3
4
5
6
7
8
9
10
11
- (id<ContentCardable>)contentCardableWithMetaData:(NSDictionary *)metaData forClassType:(ContentCardClassType)classType {
  switch (classType) {
    case ContentCardClassTypeYourValue:
      return [[CustomObject alloc] initWithMetaData:metaData classType:classType];
    case ContentCardClassTypeYourOtherValue:
      return nil;
    ...
    default:
      return nil;
  }
}

Sample use cases

There are three sample customer use cases provided. Each use case offers a detailed explanation, relevant code snippets, and a look into how Content Card variables may look and be used in the Braze dashboard:

Content Cards as supplemental content

Supplementary Content PNG

You can seamlessly blend Content Cards into an existing feed, allowing data from multiple feeds to load simultaneously. This creates a cohesive, harmonious experience with Braze Content Cards and existing feed content.

The example to the right shows a UICollectionView with a hybrid list of items that are populated via local data and Content Cards powered by Braze. With this, Content Cards can be indistinguishable alongside existing content.

Dashboard configuration

This Content Card is delivered by an API-triggered campaign with API-triggered key-value pairs. This is ideal for campaigns where the card’s values depend on external factors to determine what content to display to the user. Note that class_type should be known at set-up time.

Supplementary Content PNG

Ready to log analytics?

Visit the following section to get a better understanding of how the flow of data should look.

Content Cards in a message center


Content Cards can be used in a message center format where each message is its own card. Each message in the message center is populated via a Content Card payload, and each card contains additional key-value pairs that power on-click UI/UX. In the example below, one message directs you to an arbitrary custom view, while another opens to a webview that displays custom HTML.

Message Center PNG

Dashboard configuration

For the following message types, the key-value pair class_type should be added to your dashboard configuration. The values assigned here are arbitrary but should be distinguishable between class types. These key-value pairs are the key identifiers that the application looks at when deciding where to go when the user clicks on an abridged inbox message.

Arbitrary Custom View Message (Full Page) Webview Message (HTML)

The key-value pairs for this use case include:

- message_header set as Full Page
- class_type set as message_full_page


Message Center JPG1
The key-value pairs for this use case include:

- message_header set as HTML
- class_type set as message_webview
- message_title

This message also looks for an HTML key-value pair, but if you are working with a web domain, a URL key-value pair is also valid.

Message Center JPG2

Further explanation

The message center logic is driven by the contentCardClassType that is provided by the key-value pairs from Braze. Using the addContentCardToView method, you are able to both filter and identify these class types.

Using class_type for On Click Behavior
When a message is clicked, the ContentCardClassType handles how the next screen should be populated.

1
2
3
4
5
6
7
8
9
10
func addContentCardToView(with message: Message) {
    switch message.contentCardData?.contentCardClassType {
      case .message(.fullPage):
        loadContentCardFullPageView(with: message as! FullPageMessage)
      case .message(.webView):
        loadContentCardWebView(with: message as! WebViewMessage)
      default:
        break
    }
}

Using class_type for On Click Behavior
When a message is clicked, the ContentCardClassType handles how the next screen should be populated.

1
2
3
4
5
6
7
8
9
10
11
12
- (void)addContentCardToView:(Message *)message {
  switch (message.contentCardData.classType) {
    case ContentCardClassTypeMessageFullPage:
      [self loadContentCardFullPageView:(FullPageMessage *)message];
      break;
    case ContentCardClassTypeMessageWebview:
      [self loadContentCardWebView:(WebViewMessage *)message];
      break;
    default:
      break;
  }
}
Ready to log analytics?

Visit the following section to get a better understanding of how the flow of data should look.

Interactive Content PNG

Interactive Content Cards


Content Cards can be leveraged to create dynamic and interactive experiences for your users. In the example to the right, we have a Content Card pop-up appear at checkout providing users last-minute promotions.

Well-placed cards like this are a great way to give users a “nudge” toward specific user actions.


Dashboard configuration

The dashboard configuration for interactive Content Cards is quick and straightforward. The key-value pairs for this use case include a discount_percentage set as the desired discount amount and class_type set as coupon_code. These key-value pairs are how type-specific Content Cards get filtered and displayed on the checkout screen.

Interactive Content JPG

Ready to log analytics?

Visit the following section to get a better understanding of how the flow of data should look.

Logging impressions, clicks, and dismissals

After extending your custom objects to function as Content Cards, logging valuable metrics like impressions, clicks, and dismissals is quick and simple. This can be done through the use of a ContentCardable protocol that references and provides data to a helper file to be logged by the Braze SDK.

Implementation components

Logging Analytics
The logging methods can be called directly from objects conforming to the ContentCardable protocol.

1
2
3
customObject.logContentCardImpression()
customObject.logContentCardClicked()
customObject.logContentCardDismissed()

Retreiving the ABKContentCard
The idString passed in from your custom object is used to identify the associated Content Card to log analytics.

1
2
3
4
5
6
7
8
9
10
11
extension BrazeManager {
  func logContentCardImpression(idString: String?) {
    guard let contentCard = getContentCard(forString: idString) else { return }
 
    contentCard.logContentCardImpression()
  }
   
  private func getContentCard(forString idString: String?) -> ABKContentCard? {
    return contentCards?.first(where: { $0.idString == idString })
  }
}

Logging Analytics
The logging methods can be called directly from objects conforming to the ContentCardable protocol.

1
2
3
[customObject logContentCardImpression];
[customObject logContentCardClicked];
[customObject logContentCardDismissed];

Retreiving the ABKContentCard
The idString passed in from your custom object is used to identify the associated Content Card to log analytics.

1
2
3
4
5
6
7
8
9
10
11
- (void)logContentCardImpression:(NSString *)idString {
  ABKContentCard *contentCard = [self getContentCard:idString];
  [contentCard logContentCardImpression];
}
 
- (ABKContentCard *)getContentCard:(NSString *)idString {
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.idString == %@", idString];
  NSArray *filteredArray = [self.contentCards filteredArrayUsingPredicate:predicate];
 
  return filteredArray.firstObject;
}

Helper files

ContentCardKey Helper File
1
2
3
4
5
6
7
8
enum ContentCardKey: String {
  case idString
  case created
  case classType = "class_type"
  case dismissible
  case extras
  ...
}
1
2
3
4
5
6
static NSString *const ContentCardKeyIdString = @"idString";
static NSString *const ContentCardKeyCreated = @"created";
static NSString *const ContentCardKeyClassType = @"class_type";
static NSString *const ContentCardKeyDismissible = @"dismissible";
static NSString *const ContentCardKeyExtras = @"extras";
...
WAS THIS PAGE HELPFUL?
New Stuff!