
AppboyKit (también conocido como SDK Objective-C) ya no recibe soporte y ha sido sustituido por Swift SDK. Ya no recibirá nuevas características, correcciones de errores, actualizaciones de seguridad ni soporte técnico; sin embargo, la mensajería y los análisis seguirán funcionando con normalidad. Para obtener más información, consulta Presentación del nuevo SDK Swift de Braze.

¿Buscas la guía básica de integración del desarrollador de Content Cards? Encuéntrala en la guía básica de integración del desarrollador de Content Cards.
Guía de implementación de Content Cards
Esta guía de implementación opcional y avanzada abarca consideraciones sobre códigos de Content Cards, tres casos de uso personalizados creados por nuestro equipo, fragmentos de código que los acompañan y orientaciones sobre el registro de impresiones, clics y descartes. ¡Visita nuestro repositorio de demostraciones Braze en GitHub! Ten en cuenta que esta guía de implementación se centra en una implementación Swift, pero se proporcionan fragmentos de código Objective-C para los interesados.
Consideraciones sobre códigos
Content Cards como objetos personalizados
Al igual que un cohete que añade un propulsor, tus propios objetos personalizados se pueden ampliar para funcionar como Content Cards. Las superficies de API limitadas como esta proporcionan flexibilidad para trabajar con diferentes backends de datos de forma intercambiable. Esto se puede hacer conformando el protocolo ContentCardable e implementando el inicializador (como se muestra en los siguientes fragmentos de código) y, mediante el uso de la estructura ContentCardData, te permite acceder a los datos de ABKContentCard. La carga útil de ABKContentCard se utilizará para inicializar la estructura ContentCardData y el propio objeto personalizado, todo desde un tipo Dictionary a través del inicializador que proporciona el protocolo.
El inicializador también incluye una enumeración ContentCardClassType. Esta enumeración se utiliza para decidir qué objeto inicializar. Mediante el uso de pares clave-valor dentro del panel de Braze, puedes establecer una clave class_type explícita que se utilizará para determinar qué objeto inicializar. Estos pares clave-valor para Content Cards se reciben a través de la variable extras en ABKContentCard. Otro componente fundamental del inicializador es el parámetro de diccionario metaData. Los metaData incluyen todo lo del ABKContentCard descompuesto en una serie de claves y valores. Una vez que las tarjetas relevantes se analizan y se convierten en tus objetos personalizados, la aplicación está lista para comenzar a trabajar con ellos como si hubieran sido instanciados desde JSON o cualquier otra fuente.
Una vez que tengas una comprensión sólida de estas consideraciones sobre códigos, consulta nuestros ejemplos para empezar a implementar tus objetos personalizados.
Protocolo ContentCardable
Un objeto ContentCardData que representa los datos de ABKContentCard junto con una enumeración ContentCardClassType. Un inicializador utilizado para instanciar objetos personalizados con metadatos de ABKContentCard.
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)
}
}
Estructura de datos de Content Card
ContentCardData representa los valores analizados de un 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
}
}
Protocolo ContentCardable
Un objeto ContentCardData que representa los datos de ABKContentCard junto con una enumeración ContentCardClassType, un inicializador utilizado para instanciar objetos personalizados con metadatos de ABKContentCard.
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
Estructura de datos de Content Card
ContentCardData representa los valores analizados de un 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
Inicializador de objeto personalizado
Los metadatos de un ABKContentCard se utilizan para rellenar las variables de tu objeto. Los pares clave-valor configurados en el panel de Braze se representan en el diccionario “extras”.
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)
}
}
Identificación de tipos
La enumeración ContentCardClassType representa el valor de class_type en el panel de Braze. Este valor también se utiliza como identificador de filtro para mostrar Content Cards en diferentes lugares.
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
}
}
}
Inicializador de objeto personalizado
Los metadatos de un ABKContentCard se utilizan para rellenar las variables de tu objeto. Los pares clave-valor configurados en el panel de Braze se representan en el diccionario “extras”.
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;
}
Identificación de tipos
La enumeración ContentCardClassType representa el valor de class_type en el panel de Braze. Este valor también se utiliza como identificador de filtro para mostrar Content Cards en diferentes lugares.
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;
}
}
Solicitar Content Cards
Mientras el observador permanezca en memoria, se puede esperar la devolución de llamada de notificación del SDK de Braze.
1
2
3
4
func loadContentCards() {
BrazeManager.shared.addObserverForContentCards(observer: self, selector: #selector(contentCardsUpdated))
BrazeManager.shared.requestContentCardsRefresh()
}
Gestión de la devolución de llamada del SDK de Content Cards
Reenvía la devolución de llamada de notificación al archivo auxiliar para analizar los datos de la carga útil de tus objetos personalizados.
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
}
Trabajar con Content Cards
El class_type se pasa como filtro para devolver solo las Content Cards que tengan un class_type coincidente.
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)
}
Solicitar Content Cards
Mientras el observador permanezca en memoria, se puede esperar la devolución de llamada de notificación del SDK de Braze.
1
2
3
4
- (void)loadContentCards {
[[BrazeManager shared] addObserverForContentCards:self selector:@selector(contentCardsUpdated:)];
[[BrazeManager shared] requestContentCardsRefresh];
}
Gestión de la devolución de llamada del SDK de Content Cards
Reenvía la devolución de llamada de notificación al archivo auxiliar para analizar los datos de la carga útil de tus objetos personalizados.
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
}
Trabajar con Content Cards
El class_type se pasa como filtro para devolver solo las Content Cards que tengan un class_type coincidente.
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 @[];
}
}
Trabajar con datos de carga útil
Recorre el array de Content Cards y solo analiza las tarjetas con un class_type coincidente. La carga útil de un ABKContentCard se analiza en un 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
}
Inicializar tus objetos personalizados a partir de datos de carga útil de Content Card
El class_type se utiliza para determinar cuál de tus objetos personalizados se inicializará a partir de los datos de la carga útil.
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
}
}
Trabajar con datos de carga útil
Recorre el array de Content Cards y solo analiza las tarjetas con un class_type coincidente. La carga útil de un ABKContentCard se analiza en un 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;
}
Inicializar tus objetos personalizados a partir de datos de carga útil de Content Card
El class_type se utiliza para determinar cuál de tus objetos personalizados se inicializará a partir de los datos de la carga útil.
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;
}
}
Ejemplos
Hemos proporcionado tres ejemplos en la siguiente sección. Cada ejemplo ofrece una explicación detallada, fragmentos de código relevantes y una visión de cómo pueden verse y utilizarse las variables de Content Cards en el panel de Braze:
- Content Cards como contenido suplementario
- Content Cards en un centro de mensajes
- Content Cards interactivas
Content Cards como contenido suplementario

Puedes combinar fácilmente Content Cards en una fuente existente, permitiendo que los datos de múltiples fuentes se carguen simultáneamente. Esto crea una experiencia cohesiva y armoniosa con Content Cards de Braze y el contenido existente de la fuente.
El ejemplo adjunto muestra un UICollectionView con una lista híbrida de elementos que se rellenan a través de datos locales y Content Cards proporcionadas por Braze. Con esto, las Content Cards pueden ser indistinguibles del contenido existente.
Configuración del panel
Esta Content Card se entrega mediante una Campaign activada por API con pares clave-valor activados por API. Esto es ideal para Campaigns en las que los valores de la tarjeta dependen de factores externos para determinar qué contenido mostrar al usuario. Ten en cuenta que class_type debe conocerse en el momento de la configuración.

¿Listo para registrar análisis?
Visita la siguiente sección para comprender mejor cómo debería verse el flujo de datos.
Content Cards en un centro de mensajes
Las Content Cards se pueden utilizar en formato de centro de mensajes, donde cada mensaje es su propia tarjeta. Cada mensaje del centro de mensajes se rellena a través de la carga útil de una Content Card, y cada tarjeta contiene pares clave-valor adicionales que controlan la UI/UX al hacer clic. En el siguiente ejemplo, un mensaje te dirige a una vista personalizada arbitraria, mientras que otro abre una vista web que muestra HTML personalizado.

Configuración del panel
Para los siguientes tipos de mensajes, el par clave-valor class_type debe añadirse a la configuración de tu panel. Los valores asignados aquí son arbitrarios, pero deben ser distinguibles entre los tipos de clase. Estos pares clave-valor son los identificadores clave que la aplicación utiliza para decidir a dónde ir cuando el usuario hace clic en un mensaje resumido del buzón de entrada.
Los pares clave-valor para este ejemplo incluyen:
message_headerestablecido comoFull Pageclass_typeestablecido comomessage_full_page

Los pares clave-valor para este ejemplo incluyen:
message_headerestablecido comoHTMLclass_typeestablecido comomessage_webviewmessage_title
Este mensaje también busca un par clave-valor HTML, pero si trabajas con un dominio web, un par clave-valor de URL también es válido.

Explicación adicional
La lógica del centro de mensajes se basa en el contentCardClassType que proporcionan los pares clave-valor de Braze. Usando el método addContentCardToView, puedes filtrar e identificar estos tipos de clase.
Uso de class_type para el comportamiento al hacer clic
Cuando se hace clic en un mensaje, el ContentCardClassType determina cómo debe rellenarse la siguiente pantalla.
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
}
}
Uso de class_type para el comportamiento al hacer clic
Cuando se hace clic en un mensaje, el ContentCardClassType determina cómo debe rellenarse la siguiente pantalla.
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;
}
}
¿Listo para registrar análisis?
Visita la siguiente sección para comprender mejor cómo debería verse el flujo de datos.

Content Cards interactivas
Las Content Cards se pueden utilizar para crear experiencias dinámicas e interactivas para tus usuarios. En el ejemplo adjunto, aparece una ventana emergente de Content Card en el proceso de pago para ofrecer a los usuarios promociones de último momento.
Las tarjetas bien ubicadas como esta son una excelente forma de dar a los usuarios un “empujón” hacia acciones de usuario específicas.
Configuración del panel
La configuración del panel para las Content Cards interactivas es sencilla. Los pares clave-valor para este ejemplo incluyen un discount_percentage establecido como el monto de descuento deseado y class_type establecido como coupon_code. Estos pares clave-valor determinan cómo las Content Cards de tipo específico se filtran y muestran en la pantalla de pago.

¿Listo para registrar análisis?
Visita la siguiente sección para comprender mejor cómo debería verse el flujo de datos.
Personalización del modo oscuro
De forma predeterminada, las vistas de Content Cards responderán automáticamente a los cambios de modo oscuro en el dispositivo con un conjunto de colores temáticos.
Este comportamiento puede anularse como se detalla en nuestra guía de estilo personalizado.
Registro de impresiones, clics y descartados
Después de ampliar tus objetos personalizados para que funcionen como Content Cards, registrar métricas valiosas como impresiones, clics y descartados es rápido. Esto se puede hacer usando un protocolo ContentCardable que hace referencia y proporciona datos a un archivo auxiliar para que sean registrados por el SDK de Braze.
Componentes de implementación
Registro de análisis
Los métodos de registro se pueden llamar directamente desde objetos que se ajustan al protocolo ContentCardable.
1
2
3
customObject.logContentCardImpression()
customObject.logContentCardClicked()
customObject.logContentCardDismissed()
Recuperación de la ABKContentCard
El idString pasado desde tu objeto personalizado se utiliza para identificar la Content Card asociada y registrar análisis.
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 })
}
}
Registro de análisis
Los métodos de registro se pueden llamar directamente desde objetos que se ajustan al protocolo ContentCardable.
1
2
3
[customObject logContentCardImpression];
[customObject logContentCardClicked];
[customObject logContentCardDismissed];
Recuperación de la ABKContentCard
El idString pasado desde tu objeto personalizado se utiliza para identificar la Content Card asociada y registrar análisis.
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;
}

Para una Content Card de variante de control, aún se debe instanciar un objeto personalizado, y la lógica de la interfaz de usuario debe configurar la vista correspondiente del objeto como oculta. El objeto puede entonces registrar una impresión para informar a nuestros análisis sobre cuándo un usuario habría visto la tarjeta de control.
Archivos auxiliares
Archivo auxiliar ContentCardKey
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";
...