Skip to content



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

Fuente con una lista híbrida que mezcla datos locales y Content Cards de Braze.

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.

Los pares clave-valor del ejemplo de Content Cards como contenido suplementario. En este ejemplo, diferentes aspectos de la tarjeta como "tile_id", "tile_deeplink" y "tile_title" se configuran usando Liquid.

¿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.

Centro de mensajes con Content Cards con tarjetas de mensajes individuales.

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_header establecido como Full Page
  • class_type establecido como message_full_page

Ejemplo de mensaje de Content Card a página completa.

Los pares clave-valor para este ejemplo incluyen:

  • message_header establecido como HTML
  • class_type establecido como message_webview
  • message_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.

Content Card que abre una vista web HTML desde un par clave-valor.

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.

Una Content Card interactiva que muestra una promoción del 50 por ciento aparece en la esquina inferior izquierda de la pantalla. Tras hacer clic, se aplica una promoción al carrito.

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.

Content Card interactiva que muestra una promoción en el proceso 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;
}

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";
...
New Stuff!