Skip to main content

iOS SDK - Current (24.3.1)

Introduction

The Atomic iOS SDK is a dynamic framework for integrating an Atomic stream container (vertical or horizontal) into your iOS app, presenting cards from a stream to your customers. It is written in Objective-C.

The current stable release is 24.3.1.

Supported iOS version

The SDK supports iOS 12.0 and above.

Boilerplate app

You can use our iOS boilerplate app to help you get started with the Atomic SDK for iOS. You can download it from its GitHub repository. Alternatively, you can follow this guide.

Installation

The SDK can be installed using CocoaPods, Carthage, Swift Package Manager, or manually.

CocoaPods

  1. Add the path to the SDK spec repo to your Podfile, along with the default specs repo:
source 'https://github.com/atomic-app/action-cards-ios-sdk-specs.git'
source 'https://github.com/CocoaPods/Specs.git'
  1. Add the SDK as a dependency. You have two options available:
  • AtomicSDK: the Atomic SDK distributed as an xcframework, with support for Apple Silicon (requires Cocoapods 1.9 or above);
  • AtomicSDK-framework: the Atomic SDK distributed as a fat framework, with slices for arm64 and x86_64.
pod 'AtomicSDK', '24.3.1'
# or
pod 'AtomicSDK-framework', '24.3.1'
  1. Run pod update.

Alternative way to install Atomic SDK

Alternatively, you can install Atomic SDK through a git path. This will install the latest Atomic SDK as an xcframework.

pod 'AtomicSDK', :git => 'https://github.com/atomic-app/action-cards-ios-sdk-releases'

Carthage

  1. Add github "atomic-app/action-cards-ios-sdk-releases" to your Cartfile.
  2. Run carthage update.
  3. Follow the instructions provided by Carthage to add the SDK to your app.
fat framework

Note: As Carthage does not currently support xcframework, this will install the fat framework version, which does not include the arm64 simulator slice.

Swift Package Manager

  1. Open your Xcode project, and choose File > Add Packages.
  2. Enter https://github.com/atomic-app/action-cards-ios-sdk-releases in the upper right text field 'Search or Enter Package URL'.
  3. Set the dependency rule and click 'Add Package'.

Manual Installation

  1. You can download releases of the SDK from the Releases page on Github.
  2. Once you've downloaded the version you need, navigate to your project in Xcode and select the "General" settings tab.
  3. Drag either AtomicSDK.xcframework or AtomicSDK.framework from the directory where you unzipped the release, to the Embedded Binaries section.
  4. When prompted, ensure that "Copy items if needed" is selected, and then click "Finish".
  5. If you chose AtomicSDK.framework above, you will also need to run the strip-frameworks.sh script (downloadable from this repository) as part of a Run Script phase in your target, to get around an App Store submission bug, caused by iOS simulator architectures being present in the fat framework.
info

Note: AtomicSDK.xcframework includes support for Apple Silicon, but requires Xcode 11 or higher, while AtomicSDK.framework is a fat framework.

Setup

Before you can display a stream container or single card, you will need to configure your API base URL, environment ID and API key.

SDK API base URL

You must specify your SDK API base URL when configuring the Atomic SDK. This URL is found in the Atomic Workbench:

  1. In the Workbench, click on the cog icon in the bottom left and select 'Settings';
  2. On the screen that appears, click the 'SDK' tab. Your API base URL is displayed in the 'API Host' section.
SDK API base URL

The SDK API base URL is different to the API base URL endpoint, which is also available under Configuration. The SDK API base URL ends with client-api.atomic.io.

You can specify your SDK API base URL in two ways:

  1. By adding the following to your app's Info.plist file, replacing API_BASE_URL with your URL:

  2. By declaring your API base URL in code, replacing API_BASE_URL with your URL:

if let url = URL(string: "API_BASE_URL") {
AACSession.setApiBaseUrl(url)
}

Environment ID and API key

Within your host app, you will need to call the +initialiseWithEnvironmentId:apiKey: method to configure the SDK. Your environment ID and API key can be found in the Atomic Workbench.

If you do not call this method, and attempt to use any functionality in the SDK, an exception will be raised.

AACSession.initialise(withEnvironmentId: "<environmentId>", apiKey: "<apiKey>")

Authenticating requests using a JWT

Atomic SDK uses a JSON Web Token (JWT) to perform authentications.

The SDK Authentication guide provides step-by-step instructions on how to generate a JWT and add a public key to the Workbench.

Within your host app, you will need to call +setSessionDelegate: method to provide an object conforming to the AACSessionDelegate protocol, which contains the method:

  • Objective-C: cardSessionDidRequestAuthenticationTokenWithHandler: or
  • Swift: cardSessionDidRequestAuthenticationToken(handler:)

The SDK calls this method when it needs a JWT for authentication. You are then responsible for generating the JWT and supplying it to the SDK by calling the handler block with the token. If you do not have a valid token, pass nil to the handler, this will invoke error handling within the SDK. You must also return the token within 5 seconds, otherwise error handling will be triggered.

AACSession.setSessionDelegate(<the session delegate>)

Note: by calling +setSessionDelegate: method, the SDK holds a strong reference of the session delegate.

JWT Expiry interval

The Atomic SDK allows you to configure the time interval to determine whether the JSON Web Token (JWT) has expired. If the interval between the current time and the token's exp field is smaller than that interval, the token is considered to be expired. If this method is not set, the default value is 60 seconds. The interval must not be smaller than zero. You must return a constant value from this method, as the value is only retrieved by the SDK once.

func expiryInterval() -> TimeInterval {
return 60
}

JWT Retry interval

The Atomic SDK allows you to configure the time interval to determine whether the SDK should call the session delegate for a new JWT. The SDK won't try to call the delegate sooner than this interval after failing to fetch a valid JWT. If this method is not set, the default value is 0 seconds, which means the SDK will call for a new JWT immediately after failures. The interval must not be smaller than zero.

You must return a constant value from this method, as the value is only retrieved by the SDK once.

func retryInterval() -> TimeInterval {
return 60
}

Convenient initialization method

You can also use a convenient method +loginWithEnvironmentId:apiKey:sessionDelegate:apiBaseUrl to initialize API base URL, environment ID, session delegate and API key all at once.

It's the equivalent of calling initialiseWithEnvironmentId:apiKey:, setSessionDelegate: and setApiBaseUrl in sequence.

The following code snippet shows how to call this method.

AACSession.login(withEnvironmentId: "<environmentId>", apiKey: "<apiKey>", sessionDelegate: <the session delegate>, apiBaseUrl: <the API base URL>)

WebSockets and HTTP API Protocols

Atomic SDK uses WebSocket as the default protocol and HTTP as a backup. However, you can switch to HTTP by using setApiProtocol, which accepts a parameter of type AACApiProtocol. You can call this method at any time and it will take effect immediately. The setting will last until the host app restarts.

The AACApiProtocol enum has two values:

  • AACApiProtocolWebSockets: Represent the WebSockets protocol.
  • AACApiProtocolHttp: Represent the HTTP protocol.
AACSession.setApiProtocol(.http)

Logging out the current user

The SDK provides a method [AACSession logOut:] for clearing user-related data when a previous user logs out or when the active user changes, so that the cache (including JWT) is clear when a new user logs in to your app. This method also sends any pending analytics events back to the Atomic Platform.

Log-out behaviors

The following behaviors apply to this method:

  • After logging out, you must log in to the SDK (by calling either +[AACSession loginWithEnvironmentId:...] or the appropriate initialization methods) to proceed with another user. Otherwise, the Atomic SDK will raise exceptions.
  • This method also purges all cached card data stored by the SDK and disables all SDK activities.
  • This method also invalidates existing stream containers, single card views, and card count observers. However, they are not deallocated by the SDK in order to prevent visual flickering. For a complete log-out, you must handle deallocation yourself.
  • There is another logout method which takes an extra parameter - deregisterNotifications. Set this parameter to YES to de-register push notifications when logging out. See below for the example code snippet.

This method takes an optional parameter - completionHandler. This completion handler is invoked with a nil error object if any pending analytics events were successfully sent, or a non-nil error object if the sending of pending analytics failed.

The following code snippet shows how to call this method and handle the error.

AACSession.logout { error in
if let error = error as? NSError {
if let errorCode = AACSessionLogoutError.Code(rawValue: error.code) {
switch errorCode {
case .dataError:
// Deal with data error.
let dataError = error.userInfo[NSUnderlyingErrorKey] as? NSError
case .networkError:
// Deal with network error.
let networkError = error.userInfo[NSUnderlyingErrorKey] as? NSError
@unknown default:
// A new type of error is added in the future.
let unknownError = error.userInfo[NSUnderlyingErrorKey] as? NSError
}
}
}
}

The following code snippet shows how to de-register push notifications when logging out.

AACSession.logout(withNotificationsDeregistered: true) { error in
// The completion handler.
}

Error handling

If the error object is non-nil, the error domain will be AACSessionLogoutErrorDomain - look for a specific error code in the AACSessionLogoutErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

Displaying containers

This section applies to all types of container: vertical, horizontal and single card view.

To display an Atomic stream container in your app, create an instance of AACStreamContainerViewController. To create an instance, you must supply:

  1. A stream container ID, which uniquely identifies the stream container in the app;
  2. A configuration object, which provides initial styling and presentation information to the SDK for this stream container.

Stream container ID

First, you’ll need to locate your stream container ID.

Navigate to the Workbench, select Configuration > SDK > Stream containers and find the ID next to the stream container you are integrating.

Configurations options

The configuration object is a class of AACConfiguration, which allows you to configure a stream container, horizontal container or single card view via the following properties:

Style and presentation

  • presentationStyle: indicates how the stream container is being displayed:
    • With no button in its top left;
    • With an action button that triggers a custom action you handle. This value has no effect in horizontal container view;
    • With a contextual button, which displays Close for modal presentations, or Back when inside a navigation controller. This value has no effect in horizontal container view.
  • launchBackgroundColor: The background color to use for the launch screen, seen on the first load. Defaults to white.
  • launchTextColor: The text color to use for the view displayed when the SDK is first presented. Defaults to black at 50% opacity.
  • launchLoadingIndicatorColor: The color to use for the loading spinner on the first time loading screen. Defaults to black.
  • launchButtonColor: The color of the buttons that allow the user to retry the first load if the request fails. Defaults to black.
  • interfaceStyle: The interface style (light, dark or automatic) to apply to the stream container. Defaults to AACConfigurationInterfaceStyleAutomatic. See Dark mode for more details.
  • enabledUiElements: A bitmask of UI elements that should be enabled in the stream container. Defaults to showing toast messages and the card list header. Possible values are:
    • AACUIElementNone: No UI elements should be displayed. Do not use it in conjunction with any other values.
    • AACUIElementCardListToast: Toast messages will show at the screen's bottom. These messages pop up when cards are submitted, dismissed, snoozed, voted up/down, or if an error happens during these actions. Toast messages are also available for single card views.
    • AACUIElementCardListFooterMessage: A footer message should be displayed below the last card in the card list, if at least one is present. The message is customized using the AACCustomStringCardListFooterMessage custom string. This value has no effect in horizontal container views and single card views.
    • AACUIElementCardListHeader: The header should display at the top of the card list, allowing the user to pull down from the top of the screen to refresh the card list. This value has no effect in single card views.
  • customHeaderDelegate: An optional delegate that supports displaying a custom view on top of the card list. This delegate has no effect in horizontal card view and single card view.

Note: In Swift, to disable all UI elements, you need to set the enabledUiElements to an empty array [].

let config = AACConfiguration()
config.enabledUiElements = []

Maximum card width

(introduced in 24.1.0)

  • cardMaxWidth: You can now specify a maximum width for each card within the vertical stream container or a single card view, with center alignment for the cards.

To set this, use the cardMaxWidth property in AACConfiguration to define the desired width, and apply this configuration when initializing the stream container.

The default value for cardMaxWidth is 0, which means the card will automatically adjust its width to match that of the stream container.

However, there are a few considerations for using this property:

  • It's advised not to set the cardMaxWidth to less than 200 to avoid layout constraint warnings due to possible insufficient space for the content within the cards.

  • Any negative values for this property will be reset to 0.

  • If the specified cardMaxWidth exceeds the width of the stream container, the property will be ignored.

  • In horizontal stream containers, the cardMaxWidth property behaves the same as the cardWidth property, and it must be > 0.

The following code snippet sets the maximum card width to 500.

let config = AACConfiguration()
config.cardMaxWidth = 500

let streamContainer = AACStreamContainerViewController(identifier: "1234", configuration: config)
present(streamContainer, animated: true)

Functionality

  • cardListRefreshInterval: How frequently the card list should be automatically refreshed. Defaults to 15 seconds, and must be at least 1 second. If set to 0, the card list will not automatically refresh after the initial load. cardListRefreshInterval only applies to HTTP polling and has no effect when WebSockets is on.
Battery life

Setting the card refresh interval to a value less than 15 seconds may negatively impact device battery life and is not recommended.

  • actionDelegate: An optional delegate that handles actions triggered inside the stream container, such as the tap of the custom action button in the top left of the stream container, or submit and link buttons with custom actions.
  • runtimeVariableDelegate: An optional runtime variable delegate that resolves runtime variable for the cards.
  • cardEventDelegate: An optional delegate that responds to card events in the stream container.
  • runtimeVariableResolutionTimeout: The maximum amount of time, in seconds, allocated to the resolution of runtime variables in your runtimeVariableDelegate's cardSessionDidRequestRuntimeVariables:completionHandler: method. If you do not call the provided completionHandler passed to this method before the timeout is reached, the default values for all runtime variables will be used. If you do not implement this delegate method, this property is not used. Defaults to 5 seconds.
  • cardVotingOptions: A bitmask representing the voting options that a user can choose from in a card's overflow menu. Voting options allow a user to flag a card as useful or not useful.

Custom strings

The configuration object also allows you to specify custom strings for features in the SDK, using the setValue:forCustomString: method:

  • AACCustomStringCardListTitle: The title for the card list in this stream container - defaults to "Cards".
  • AACCustomStringCardSnoozeTitle: The title for the feature allowing a user to snooze a card - defaults to "Remind me".
  • AACCustomStringAwaitingFirstCard: The message displayed over the card list, when the user has never received a card before - defaults to "Cards will appear here when there’s something to action."
  • AACCustomStringAllCardsCompleted: The message displayed when the user has received at least one card before, and there are no cards to show - defaults to "All caught up".
  • AACCustomStringVotingUseful: The title to display for the action a user taps when they flag a card as useful - defaults to "This is useful".
  • AACCustomStringVotingNotUseful: The title to display for the action a user taps when they flag a card as not useful - defaults to "This isn't useful".
  • AACCustomStringVotingFeedbackTitle: The title to display at the top of the screen allowing a user to provide feedback on why they didn't find a card useful - defaults to "Send feedback".
  • AACCustomStringCardListFooterMessage: The message to display below the last card in the card list, provided there is at least one present. Does not apply in horizontal container and single card view, and requires enabledUiElements to contain AACUIElementCardListFooterMessage. Defaults to an empty string.
  • AACCustomStringNoInternetConnectionMessage: The error message shown when the user does not have an internet connection. Defaults to "No internet connection".
  • AACCustomStringDataLoadFailedMessage: The error message shown when the theme or card list cannot be loaded due to an API error. Defaults to "Couldn't load data".
  • AACCustomStringTryAgainTitle: The title of the button allowing the user to retry the failed request for the card list or theme. Defaults to "Try again".
  • AACCustomStringToastCardDismissedMessage: Customized toast message for when the user dismisses a card - defaults to "Card dismissed".
  • AACCustomStringToastCardCompletedMessage: Customized toast message for when the user completes a card - defaults to "Card completed".
  • AACCustomStringToastCardSnoozeMessage: Customized toast messages for when the user snoozes a card - defaults to "Snoozed until X" where X is the time the user dismissed the card until.
  • AACCustomStringToastCardFeedbackMessage: Customized toast message for when the user sends feedback (votes) for a card - defaults to "Feedback received".

(the following options are introduced in 24.3.0)

  • AACCustomStringProcessingStateMessage: The message displayed on the upload processing overlay during file upload. Defaults to "Sending, please wait...".
  • AACCustomStringProcessingStateCancelButtonTitle: The text displayed on the cancel button in the upload overlay during file upload. Defaults to "Cancel process".
  • AACCustomStringToastFileUploadFailedMessage: Customised toast message shown when file(s) fail to upload during card submission. Defaults to "Couldn't upload file(s)".
  • AACCustomStringRequestCameraAccessMessage: Customised toast message shown when requesting camera access from the user. Defaults to "Access to your camera is required to take photos. Please enable camera access in your device settings".
  • AACCustomStringRequestCameraAccessSettingsTitle: The title for the button in the toast message prompting for camera access, which navigates to the Settings app. Defaults to Settings.

Displaying a vertical container

To display a vertical container, you do not need to supply any additional parameters.

Creating a vertical stream container

You can now create a stream container by supplying the stream container ID and configuration object on instantiation:

let config = AACConfiguration()
config.presentationStyle = .withContextualButton
config.launchBackgroundColor = .white
config.launchIconColor = .blue
config.launchButtonColor = .blue
config.launchTextColor = .white

let streamContainer = AACStreamContainerViewController(identifier: "1234", configuration: config)
present(streamContainer, animated: true)

Displaying a custom header

Only for vertical stream containers

Custom header is only available for AACStreamContainerViewController. It has no effect in the single card view and horizontal stream container.

You can provide a custom UIView to the SDK as a header that scrolls up along with the cards. This custom view is placed below the built-in header if the built-in one is enabled. It is provided by your app via the -streamContainerDidRequestCustomHeader:proposedWidth: on AACStreamContainerHeaderDelegate delegate. You pass an object conforming to AACStreamContainerHeaderDelegate to AACConfiguration.customHeaderDelegate when creating a stream container.

You are responsible for managing the layout of the header before passing it to the SDK. The -streamContainerDidRequestCustomHeader:proposedWidth: method, when called by the SDK, provides you with a width that indicates the displaying area of the header. It's your responsibility to lay out the header based on that width and resize its height accordingly. Once it's done, pass it back to the SDK.

There are no restrictions for views in the header as long as they are supported by iOS, which can also display dynamic contents. However, you should not change its size unless the SDK calls this method.

The simplest way to create a custom header is through pure code. Here is an example.

// 1. Define the delegate.
@objcMembers class ACSSwiftCustomHeaderDelegate: NSObject, AACStreamContainerHeaderDelegate {

private override init() {
super.init()
}

// The singleton is only for demonstration. It's not compulsory.
static let sharedInstance = ACSSwiftCustomHeaderDelegate()

func tapped() {
print("You tapped the button.")
}

func streamContainerDidRequestCustomHeader(_ streamContainer: AACStreamContainerViewController, proposedWidth: CGFloat) -> UIView {

let topView = UIView(frame: CGRect(x: 0, y: 10, width: proposedWidth, height: 90))
topView.backgroundColor = .blue

let title1 = UILabel(frame: CGRect(x: 30, y: 20, width: 0, height: 0))
title1.text = "Name: John Doe"
title1.textColor = .white
title1.sizeToFit()
topView.addSubview(title1)

let title2 = UILabel(frame: CGRect(x: 30, y: 45, width: 0, height: 0))
title2.text = "Container ID: \(streamContainer.identifier)"
title2.textColor = .white
title2.sizeToFit()
topView.addSubview(title2)

let button = UIButton(frame: CGRect(x: 10, y: 110, width: proposedWidth - 20, height: 40))
button.backgroundColor = .red
button.setTitle("Tap this", for: .normal)
button.addTarget(self, action: #selector(ACSSwiftCustomHeaderDelegate.tapped), for: .touchUpInside)

let header = UIView(frame: CGRect(x: 0, y: 0, width: proposedWidth, height: 160))
header.backgroundColor = .clear
header.addSubview(topView)
header.addSubview(button)
return header
}
}

// 2. Assign the delegate to the configuration
let config = AACConfiguration()
config.customHeaderDelegate = ACSSwiftCustomHeaderDelegate.sharedInstance

You can also create the view using the storyboard. Then pass the instantiated view to the SDK.

Displaying a horizontal stream container

The Atomic iOS SDK also supports rendering a horizontally laid stream container in your host app. The horizontal view renders cards from left to right.

Create an instance of AACHorizontalContainerView, which is a UIView that is configured in the same way as a stream container. On instantiation, you supply the following parameters:

  1. The ID of the stream container to render in the horizontal container view.
  2. A configuration object, which provides initial styling and presentation information to the SDK for the horizontal container view.

The configuration options, supplied using the configuration object above, are a type of AACHorizontalContainerConfiguration, which is the subclass of AACConfiguration. You must specify its cardWidth (that is > 0) before passing it, otherwise an exception will be raised.

let config = AACHorizontalContainerConfiguration()
config.cardWidth = 400

let horizontalView = AACHorizontalContainerView(frame: self.view.bounds, containerIdentifier: "1234", configuration: config)
self.view.addSubview(horizontalView)

Pull to refresh functionality is disabled.

You can set a delegate (conforming to AACHorizontalContainerViewDelegate) on the horizontal container view to be notified when the view changes height, either because a card is submitted, dismissed or snoozed, or because a new card arrived into the container view. This allows you to animate changes to the intrinsicContentSize of the horizontal container view.

func horizontalContainerView(_ containerView: AACHorizontalContainerView, willChange newSize: CGSize) {
// Perform animation here.
}

Configuration options for a horizontal stream container

Other than properties inherited from AACConfiguration, the configuration object allows you to configure a horizontal container view via the following properties:

  • cardWidth: The width of each card in this horizontal container view. It must be > 0. Cards in horizontal container view all have the same width that must be assigned explicitly. The width of the view itself can not be used to determine the card width anymore.

  • emptyStyle: The style of the empty state (when there are no cards) for the horizontal container. Possible values are:

    • AACHorizontalContainerConfigurationEmptyStyleStandard: Default value. The horizontal container displays a no-card user interface.
    • AACHorizontalContainerConfigurationEmptyStyleShrink: The horizontal container shrinks itself.
  • headerAlignment: The alignment of the title in the horizontal header. Possible values are:

    • AACHorizontalContainerConfigurationHeaderAlignmentCenter: Default value. The title is aligned in the middle of the header.
    • AACHorizontalContainerConfigurationHeaderAlignmentLeft: The title is aligned to the left of the header.
  • scrollMode: The scrolling behaviors in the horizontal container. Possible values are:

    • AACHorizontalContainerConfigurationScrollModeSnap: Default value. The container scrolls over one card at a time. The card is placed in the middle of the viewport when the scrolling terminates.
    • AACHorizontalContainerConfigurationScrollModeFree: The container scrolls freely.
  • lastCardAlignment: The alignment of the card when it is the only one in the horizontal container. Possible values are:

    • AACHorizontalContainerConfigurationLastCardAlignmentLeft: Default value. The last card is aligned to the left of the container.
    • AACHorizontalContainerConfigurationLastCardAlignmentCenter: The last card is aligned in the middle of the container.
Exceptions
  1. AACUIElementCardListFooterMessage and AACCustomStringCardListFooterMessage have no effects in horizontal container view.
  2. presentationStyle is always AACConfigurationPresentationStyleWithoutButton in horizontal container view.

Displaying a single card

The Atomic iOS SDK also supports rendering a single card in your host app.

To create an instance of AACSingleCardView, which is a UIView that is configured in the same way as a stream container, you supply the following parameters on instantiation:

  1. The ID of the stream container to render in the single card view. The single card view renders only the first card that appears in that stream container;
  2. A configuration object, which provides initial styling and presentation information to the SDK for the single card view.

The configuration options, supplied using the configuration object above, are the same as those for a stream container. The only configuration option that does not apply is presentationStyle, as the single card view does not display a header, and therefore does not show a button in its top left.

let config = AACConfiguration()
config.launchBackgroundColor = .white
config.launchIconColor = .blue
config.launchButtonColor = .blue
config.launchTextColor = .white

let cardView = AACSingleCardView(frame: view.bounds, containerIdentifier: "1234", configuration: config)
view.addSubview(cardView)

Pull to refresh functionality is disabled for single card view.

You can set a delegate (conforming to AACSingleCardViewDelegate) on the single card view to be notified when the view changes height, either because a card is submitted, dismissed or snoozed, or because a new card arrived into the single card view (if polling is enabled or is using WebSockets). This allows you to animate changes to the intrinsicContentSize of the single card view.

func singleCardView(_ cardView: AACSingleCardView, willChange newSize: CGSize) {
// Perform animation here.
}

Configuration options for the single card view

There is a subclass of AACConfiguration - AACSingleCardConfiguration - which can be used to enable features that only apply to single card view.

let config = AACSingleCardConfiguration()
config.automaticallyLoadNextCard = true

Available features are:

  • automaticallyLoadNextCard: When enabled, will automatically display the next card in the single card view if there is one, using a locally cached card list. Defaults to NO.

Toasts for single card view

You can display toast messages for a single card view, just like with other container types. Use enabledUiElements in AACSingleCardConfiguration to choose if you want to show these toast messages.

enabledUiElements is a bitmask indicating which UI elements are active in the stream container. By default, toast messages are displayed. For a single card view, the possible values are:

  • AACUIElementNone: Don't display any UI elements. This shouldn't be mixed with other values.
  • AACUIElementCardListToast: Toast messages will show at the screen's bottom. These messages pop up when cards are submitted, dismissed, snoozed, voted up/down, or if an error happens during these actions.
let config = AACSingleCardConfiguration()
config.enabledUiElements = .cardListToast
let singleCardView = AACSingleCardView(frame: self.view.frame, containerIdentifier: "1234", configuration: config)
self.view.addSubview(singleCardView)

To disable the toast, simply set enabledUiElements to AACUIElementNone, or [] in Swift.

let config = AACSingleCardConfiguration()
config.enabledUiElements = []

Closing a stream container

Vertical stream containers, horizontal stream containers and single card views are dismissed of like other views or controllers. There is no specific method that needs be to called.

Customizing the first time loading behavior

When a stream container with a given ID is launched for the first time on a user's device, the SDK loads the theme and caches it for future use. On subsequent launches of the same stream container, the cached theme is used and the theme is updated in the background, for the next launch. Note that this first-time loading screen is not presented in single card view and horizontal container view - if those views fail to load, they collapse to a height of 0.

The SDK supports some basic properties to style the first-time load screen, which displays a loading spinner in the center of the container. If the theme or card list fails to load for the first time, an error message is displayed with a 'Try again' button. One of two error messages is possible - 'Couldn't load data' or 'No internet connection'.

First-time loading screen colors are customized using the following properties on AACStreamContainer:

  • launchBackgroundColor: The background color to use for the launch screen, seen on the first load. Defaults to white.
  • launchTextColor: The text color to use for the view displayed when the SDK is first presented. Defaults to black at 50% opacity.
  • launchLoadingIndicatorColor: The color to use for the loading spinner on the first-time loading screen. Defaults to black.
  • launchButtonColor: The color of the buttons that allow the user to retry the first load if the request fails. Defaults to black.

You can also customize the text for the first load screen error messages and the 'Try again' button, using the setValue:forCustomString: method of AACConfiguration.

Note: These customized error messages also apply to the card list screen.

  • AACCustomStringNoInternetConnectionMessage: The error message shown when the user does not have an internet connection. Defaults to "No internet connection".
  • AACCustomStringDataLoadFailedMessage: The error message shown when the theme or card list cannot be loaded due to an API error. Defaults to "Couldn't load data".
  • AACCustomStringTryAgainTitle: The title of the button allowing the user to retry the failed request for the card list or theme. Defaults to "Try again".

API-driven card containers

(introduced in 23.4.0)

In version 23.4.0, we've introduced a new feature for observing stream containers with pure SDK API, even when that container's UI is not loaded into memory.

When you opt to observe a stream container, it is updated by default immediately after any changes in the published cards. Should the WebSocket be unavailable, the cards are updated at regular intervals, which you can specify. Upon any change in cards, the handler block is executed with the updated card list or nil if the cards couldn't be fetched. Note that the specified time interval for updates cannot be less than 1 second.

The following code snippet shows the simplest use case scenario:

// Observe the stream container
AACSession.observeStreamContainer(identifier: "1", configuration: nil) { cards in
if let cards = cards {
print("There are \(cards.count) cards in the container.")
}
}

This method returns a token that you can use to stop the observation, see Stopping the observation for more details.

Card instance class clusters

In the callback, the cards parameter is an array of AACCardInstance objects. Each AACCardInstance contains a variety of other class types that represent the card elements defined in Workbench. Detailed documentation for the classes involved in constructing an AACCardInstance object is not included in this guide. However, you can refer to the examples provided below, which demonstrate several typical use cases.

Configuration options

The method accepts an optional configuration parameter. The configuration object, AACStreamContainerObserverConfiguration, allows you to customize the observer's behavior with the following properties, which are all optional:

  • cardListRefreshInterval: defines how frequently the system checks for updates when the WebSocket service is unavailable. The default interval is 15 seconds, but it must be at least 1 second. If a value less than 1 second is specified, it defaults to 1 second.
  • filters: filters applied when fetching cards for the stream container. It defaults to nil, meaning no filters are applied. See Filtering cards for more details of stream filtering.
  • runtimeVariableDelegate: a delegate for resolving runtime variables for the cards. Defaults to nil. See Runtime variables for more details of runtime variables.
  • runtimeVariableResolutionTimeout: the maximum time allocated for resolving variables in the delegate. If the tasks within the delegate method exceed this timeout, or if the completionHandler is not called within this timeframe, default values will be used for all runtime variables. The default timeout is 5 seconds and it cannot be negative.
  • sendRuntimeVariableAnalytics: whether the runtime-vars-updated analytics event, which includes the resolved values of each runtime variable, should be sent upon resolution. The default setting is NO. If you set this flag to YES, ensure that the resolved values of your runtime variables do not contain sensitive information that shouldn't appear in analytics. See SDK analytics for more details on runtime variable analytics.

Stopping the observation

The observer ceases to function when you call +[AACSession logout:]. Alternatively, you can stop the observation using the token returned from the observation call mentioned above:

let token = AACSession.observeStreamContainer(identifier: "1", configuration: nil) { _ in }
AACSession.stopObservingStreamContainer(token: token)

Examples

Accessing card metadata

Card metadata encompasses data that, while not part of the card's content, are still critical pieces of information. Key metadata elements include:

  • Card instance ID: This is the unique identifier assigned to a card upon its publication.
  • Card priority: Defined in the Workbench, this determines the card's position within the feed. The priority will be an integer between 1 & 10, a priority of 1 indicates the highest priority, placing the card at the top of the feed.
  • Action flags: Also defined in the Workbench, these flags dictate the visibility of options such as dismissing, snoozing, and voting menus for the card.

The code snippet below shows how to access these metadata elements for a card instance.

AACSession.observeStreamContainer(identifier: "1", configuration: nil) { cards in
if let card = cards?.first {
print("The card instance ID is \(card.detail.cardId)")
print("The priority of the card is \(card.metadata.priority)")
print("The card has dismiss overflow menu \(card.actionFlags.dismissOverflowDisabled ? "disabled" :"enabled").")
}
}

Traversing card elements

Elements refer to the contents that are defined in the Workbench on the Content page of a card. All elements that are at the same hierarchical level within a card are encapsulated in an AACCardLayout object. Elements at the top level are accessible through the defaultLayout property of the AACCardInstance. The nodes property within defaultLayout contains them.

The code snippet below shows how to traverse through all the elements in a card and extract the text representing the card's category.

AACSession.observeStreamContainer(identifier: "1", configuration: nil) { cards in
if let card = cards?.first {
for element in card.defaultLayout.nodes {
if let category = element as? AACCardNodeCategory {
print("The category of card \(card.detail.cardId) is \(category.text).")
}
}
}
}

Note: There are several classes derived from AACCardNode that correspond to the various elements you can create in Workbench. Detailed documentation for all these classes is not provided here. For information on each class, please refer to the documentation in the header files.

Accessing subviews

Subviews are layouts that differ from the defaultLayout and each has a unique subview ID. See Link to subview on how to get the subview ID.

The following code snippet shows how to retrieve a subview layout using a specific subview ID.

AACSession.observeStreamContainer(identifier: "1", configuration: nil) { cards in
if let card = cards?.first, let subview = card.layout(withName: "subviewID") {
print("Accessing subview \(subview.title).")
}
}

API-driven card actions

(introduced in 23.4.0)

In version 23.4.0, we've introduced a new feature that execute card actions through pure SDK API. The currently supported actions are: dismiss, submit, and snooze. To execute these card actions, follow these two steps:

  1. Create a card action object: Use the corresponding initialization methods of the AACSessionCardAction class. You'll need at least a container ID and a card instance ID for this. The container ID should be from the container where the card is published, and the card instance ID can be obtained from the API-driven stream container. See API-driven card containers for more details.

  2. Execute the action: Call the method +[AACSession onCardAction:completionHandler:] to perform the card action.

Dismissing a card

The following code snippet shows how to dismiss a card.

let action = AACSessionCardAction(dismissActionWithContainerId: "1", cardId: "card-id")
AACSession.onCardAction(action) { error in
if let error = error {
print("An error happened when dismissing the card.")
} else {
print("Card dismissed.")
}
}

Submitting a Card

You have the option to submit certain values along with a card. These values are optional and should be encapsulated in an NSDictionary object, using NSString keys and values that are either strings, numbers, or booleans.

Input Components

While editing cards in Workbench, you can add input components onto cards and apply various validation rules, such as Required, Minimum length, or Maximum length. The input elements can be used to submit user-input values, where the validation rules are applied when submitting cards through UIs of stream containers.

However, for this non-UI version, support for input components is not available yet. There is currently no mechanism to store values in these input components through this API, and the specified validation rules won't be enforced when submitting cards.

Submitting a card in SDK 24.2.0

As of version 24.2.0, Atomic cards include button names when they are submitted. The button name will be added to analytics to enable referencing the triggering button in an Action Flow. Therefore, you need to provide a button name when submitting cards.

Getting the button name

The button name of a submit button can be acquired when receiving cards through API-driven cards. The following code snippet shows how to obtain button name(s) from the top-level of the first card.

AACSession.observeStreamContainer(identifier: "1", configuration: nil) { cards in
if let card = cards?.first {
// Pass `nil` for top-level buttons, or a subview ID for subview buttons.
let buttons = card.buttons(withSubviewId: nil)
for button in buttons {
if let submitButton = button as? AACCardNodeSubmitButton, let buttonName = submitButton.buttonName {
print("The button name of submit button \(submitButton.text) is \(buttonName)")
}
}
}
}
Submitting the card

With button name obtained, you can now submit the card. The following code snippet shows how to submit a card with specific values.

let action = AACSessionCardAction(submitActionWithContainerId: "1", cardId: "card-id", submitButtonName: "button-name", submitValues: [
"submit-key": "submitted values",
"submit-key2": 999
])
AACSession.onCardAction(action) { error in
if let error = error {
print("An error happened when submitting the card.")
} else {
print("Card submitted.")
}
}

Submitting a card prior SDK version 24.2.0

Before version 24.2.0, you don't need a button name when submitting cards. The following code snippet shows how to submit a card with specific values.

let action = AACSessionCardAction(submitActionWithContainerId: "1", cardId: "card-id", submitValues: [
"submit-key": "submitted values",
"submit-key2": 999
])
AACSession.onCardAction(action) { error in
if let error = error {
print("An error happened when submitting the card.")
} else {
print("Card submitted.")
}
}

Snoozing a Card

When snoozing a card, you must specify a non-negative interval in seconds. Otherwise an error will be returned.

The following code snippet shows how to snooze a card for a duration of 1 minute.

let action = AACSessionCardAction(snoozeActionWithContainerId: "1", cardId: "card-id", snoozeInterval: 60)
AACSession.onCardAction(action) { error in
if let error = error {
print("An error happened when snoozing the card.")
} else {
print("Card snoozed.")
}
}

Error handling

If the action executes successfully, the error object in the completion handler will be nil. If there's an error, it will be returned with the error domain set as AACSessionCardActionsErrorDomain.

To identify the specific cause of the error, refer to the AACSessionCardActionsErrorCode enumeration for the relevant error code. Additionally, the NSUnderlyingErrorKey will be populated in the userInfo dictionary of the error, providing further details.

Dark mode

Stream containers in the Atomic iOS SDK support dark mode. You configure an (optional) dark theme for your stream container in the Atomic Workbench.

The interface style determines which theme is rendered:

  • AACConfigurationInterfaceStyleAutomatic: If the user's device is currently set to light mode, the stream container will use the light (default) theme. If the user's device is currently set to dark mode, the stream container will use the dark theme (or fallback to the light theme if this has not been configured). On iOS versions less than 13, this setting is equivalent to AACConfigurationInterfaceStyleLight.
  • AACConfigurationInterfaceStyleLight: The stream container will always render in light mode, regardless of the device setting.
  • AACConfigurationInterfaceStyleDark: The stream container will always render in dark mode, regardless of the device setting.

To change the interface style, set the corresponding value for the interfaceStyle property on the AACConfiguration object when creating the stream container.

If this property is left unset, it will default to AACConfigurationInterfaceStyleAutomatic.

Filtering cards

Stream containers (vertical or horizontal), single card views and container card count observers can have one or more filters applied. These filters determine which cards are displayed, or how many cards are counted.

A stream container filter consists of two parts: a filter value and an operator.

Filter values

The filter value is used to filter cards in a stream container. The following list outlines all card attributes that can be used as a filter value.

Card attributeDescriptionValue type
PriorityCard priority defined in Workbench, Card -> DeliveryNSInteger
Card template created dateThe date time when a card template is createdNSDate
Card template IDThe template ID of a card, see below for how to get itNSString
Card template nameThe template name of a cardNSString
Custom variableThe variables defined for a card in Workbench, Card -> VariablesMultiple

Use corresponding static methods of AACCardFilterValue to create a filter value.

Examples

Card priority

The following code snippet shows how to create a filter value that represents a card priority 6.

let filterValue = AACCardFilterValue.byPriority(6)
Custom variable

The following code snippet shows how to create a filter value that represents a boolean custom variable isSpecial.

let filterValue = AACCardFilterValue.byVariableName("isSpecial", boolean: true)

Note: It's important to specify the right value type when referencing custom variables for filter value. There are five types of variables in the Workbench, currently four are supported: String, Number, Date and Boolean. You can use methods like +[AACCardFilterValue byVariableName:string:] to create them respectively.

How to get the card template ID

On the card editing page, click on the ID part of the overflow menu at the upper-right corner.

Card template ID

Filter operators

The operator is the operational logic applied to a filter value (some operators require 2 or more values).

The following table outlines available operators.

OperatorDescriptionSupported types
equalToEqual to the filter valueNSInteger, NSDate, NSString, BOOL
notEqualToNot equal to the filter valueNSInteger, NSDate, NSString, BOOL
greaterThanGreater than the filter valueNSInteger, NSDate
greaterThanOrEqualToGreater than or equal to the filter valueNSInteger, NSDate
lessThanLess than the filter valueNSInteger, NSDate
lessThanOrEqualToLess than or equal to the filter valueNSInteger, NSDate
inIn one of the filter valuesNSInteger, NSDate, NSString
notInNot in one of the filter valuesNSInteger, NSDate, NSString
betweenIn the range of start and end, inclusiveNSInteger, NSDate

After creating a filter value, use the corresponding static method on the AACCardListFilter class to combine it with an operator.

Examples

Card priority range

The following code snippet shows how to create a filter that filters card with priority between 2 and 6 inclusive.

let filterValue1 = AACCardFilterValue.byPriority(2)
let filterValue2 = AACCardFilterValue.byPriority(6)
let filter = AACCardListFilter.filter(byCardsBetweenStartValue: filterValue1, endValue: filterValue2)
Passing correct value type to an operator

Each operator supports different type of values. For example, operator lessThan only support NSInteger and NSDate. So passing NSString values to that operator will raise an exception.

Applying filters to a stream container or a card count observer

There are three steps to filter cards in a stream container or for a card count observer:

  1. Create one or more AACCardFilterValue objects.

  2. Combine filter values with filter operators to form a AACCardFilter.

  3. Apply filter(s).

    3.1. For stream containers, call -[AACStreamContainer applyFilters:] to apply multiple filters, otherwise call -[AACStreamContainer applyFilter:]. To delete all existing filters, pass either nil or an empty list [] to the applyFilters method, or nil to the applyFilter method.

    3.2. For card count observers, pass an array of filters to parameter filters when creating an observer using +[AACSession:observeCardCountForStreamContainerWithIdentifier:];

Examples

Card priority 5 and above

The following code snippet shows how to only display cards with priority > 5 in a stream container.

let filterValue = AACCardFilterValue.byPriority(5)
let filter = AACCardListFilter.filter(byCardsGreaterThan: filterValue)

...

// Acquire the stream container object and call the filtering method.
streamContainer.apply(filter)
Earlier than a set date

The following code snippet shows how to only display cards created earlier than 9/Jan/2023 inclusive in a stream container.

let filterValue = AACCardFilterValue.byCreatedDate(Date(timeIntervalSince1970: 1673226000))
let filter = AACCardListFilter.filter(byCardsLessThanOrEqualTo: filterValue)

...

streamContainer.apply(filter)
Card template names

The following code snippet shows how to only display cards with the template names 'card 1', 'card 2', or 'card 3' in a stream container.

let filterValue1 = AACCardFilterValue.byCardTemplateName("card1")
let filterValue2 = AACCardFilterValue.byCardTemplateName("card2")
let filterValue3 = AACCardFilterValue.byCardTemplateName("card3")
let filter = AACCardListFilter.filter(byCardsIn: [filterValue1, filterValue2, filterValue3])

...

streamContainer.apply(filter)
Combination of filter values

The following code snippet shows how to only display cards with priority != 6 and custom variable isSpecial == true in a stream container.

Note: isSpecial is a Boolean custom variable defined in Workbench.

let filterValue1 = AACCardFilterValue.byPriority(6)
let filter1 = AACCardListFilter.filter(byCardsNotEqualTo: filterValue1)

let filterValue2 = AACCardFilterValue.byVariableName("isSpecial", boolean: true)
let filter2 = AACCardListFilter.filter(byCardsEqualTo: filterValue2)

...

streamContainer.apply([filter1, filter2])

Legacy filter

The legacy filter is still supported - AACCardListFilter.filter(byCardInstanceId:). This filter requests that the stream container or single card view show only a card matching the specified card instance ID, if it exists. An instance of this filter can be created using the corresponding static method on the AACCardListFilter class.

The card instance ID can be found in the push notification payload, allowing you to apply the filter in response to a push notification being tapped.

let filter = AACCardListFilter.filter(byCardInstanceId: "ABCD-1234")
streamContainer.apply(filter)

Removing all filters

  • For stream containers, specify nil or an empty list [] to the -[AACStreamContainer applyFilters:] method, or nil to the -[AACStreamContainer applyFilter:] method.
  • For card count observers, create a new card counter observer with nil to the filters parameter.

In the Atomic Workbench, you can create a submit or link button with a custom action payload.

  • When such a link button is tapped, the streamContainerDidTapLinkButton:withAction: method is called on your action delegate.
  • When such a submit button is tapped, and after the card is successfully submitted, the streamContainerDidTapSubmitButton:withAction: method is called on your action delegate.

The second parameter to each of these methods is an action object, containing the payload that was defined in the Workbench for that button. You can use this payload to determine the action to take, within your app, when the submit or link button is tapped.

The action object also contains the card instance ID and stream container ID where the custom action was triggered.

// 1. Assign the action delegate
let config = AACConfiguration()
config.actionDelegate = self

// 2. Implement the callbacks
func streamContainerDidTapLinkButton(_ streamContainer: AACStreamContainerViewController, with action: AACCardCustomAction) {
if let screenName = action.actionPayload["screen"] as? String, screenName == "home-screen" {
// Perform an action
}
}

func streamContainerDidTapSubmitButton(_ streamContainer: AACStreamContainerViewController, with action: AACCardCustomAction) {
if let outcome = action.actionPayload["outcome"] as? String, outcome == "success" {
// Perform an action
}
}

Customizing toast messages for card events

You can customize any of the toast messages used when dismissing, completing, snoozing and placing feedback on a card. This is configurable for each stream container. You simply supply a string for each custom message. If you do not supply a string, the defaults will be used. See the Custom strings section to read more about other custom strings that can be specified in the configuration object using the setValue:forCustomString: method.

Options are:

  • AACCustomStringToastCardDismissedMessage: Customized toast message for when the user dismisses a card - defaults to "Card dismissed".
  • AACCustomStringToastCardCompletedMessage: Customized toast message for when the user completes a card - defaults to "Card completed".
  • AACCustomStringToastCardSnoozeMessage: Customized toast messages for when the user snoozes a card - defaults to "Snoozed until X" where X is the time the user dismissed the card until.
  • AACCustomStringToastCardFeedbackMessage: Customized toast message for when the user sends feedback (votes) for a card - defaults to "Feedback received".

(the following options are introduced in 24.3.0)

  • AACCustomStringToastFileUploadFailedMessage: Customised toast message shown when file(s) fail to upload during card submission. Defaults to "Couldn't upload file(s)".
  • AACCustomStringRequestCameraAccessMessage: Customised toast message shown when requesting camera access from the user. Defaults to "Access to your camera is required to take photos. Please enable camera access in your device settings".
  • AACCustomStringRequestCameraAccessSettingsTitle: The title for the button in the toast message prompting for camera access, which navigates to the Settings app. Defaults to Settings.

Card snoozing

The Atomic SDKs provide the ability to snooze a card from a stream container or single card view. Snooze functionality is exposed through the card’s action buttons, overflow menu and the quick actions menu (exposed by swiping a card to the left, on iOS and Android).

Tapping on the snooze option from either location brings up the snooze date and time selection screen. The user selects a date and time in the future until which the card will be snoozed. Snoozing a card will result in the card disappearing from the user’s card list or single card view, and reappearing again at the selected date and time. A user can snooze a card more than once.

When a card comes out of a snoozed state, if the card has an associated push notification, and the user has push notifications enabled, the user will see another notification, where the title is prefixed with Snoozed:.

You can customize the title of the snooze functionality, as displayed in a card’s overflow menu and in the title of the card snooze screen. The default title, if none is specified, is Remind me.

On the AACConfiguration object, call the setValue:forCustomString: method to customize the title for the card snooze functionality:

config.setValue("Snooze", for: .cardSnoozeTitle)

Prevent Snoozing Beyond Expiry Date

(introduced in 24.3.0)

Selecting a date & time combination beyond a card's expiry date is now prevented when snoozing a card using the SDK's built-in interface.

There are three ways to snooze a card in the Atomic SDK:

  1. Overflow Menu: Select the overflow menu item (default "Remind me"), then choose a date & time in the built-in selector.
  2. Snooze Button: Tap a snooze button on the card and select a date & time via the built-in selector.
  3. Pre-set Snooze Button: Tap a snooze button with a pre-set snooze period, which snoozes the card immediately without showing the selector.

If an expiry date is set on the card, you cannot select dates beyond this expiry in scenarios 1 and 2. Scenario 3 remains unaffected, as the snooze period is explicitly pre-configured in Workbench.

Card voting

The Atomic SDKs support card voting, which allows you to gauge user sentiment towards the cards you send. When integrating the SDKs, you can choose to enable options for customers to indicate whether a card was useful to the user or not, accessible when they tap on the overflow button in the top right of a card.

If the user indicates that the card was useful, a corresponding analytics event is sent for that card (card-voted-up).

If they indicate that the card was not useful, they are presented with a secondary screen where they can choose to provide further feedback. The available reasons for why a card wasn’t useful are:

  • It’s not relevant;
  • I see this too often;
  • Something else.

If they select "Something else", a free-form input is presented, where the user can provide additional feedback. The free form input is limited to 280 characters. After tapping "Submit", an analytics event containing this feedback is sent (card-voted-down).

You can customize the titles that are displayed for these actions, as well as the title displayed on the secondary feedback screen. By default these are:

  • Thumbs up - "This is useful";
  • Thumbs down - "This isn’t useful";
  • Secondary screen title - "Send feedback".

Card voting is disabled by default. You can enable positive card voting ("This is useful"), negative card voting ("This isn’t useful"), or both:

let config = AACConfiguration()
config.cardVotingOptions = [.notUseful, .useful] // Enable both voting options
config.cardVotingOptions = [.useful] // Enable one voting option
config.cardVotingOptions = [.none] // Enable no voting options (default)

You can also customize the titles for the card voting options, and the title displayed at the top of the feedback screen, presented when a user indicates the card wasn’t useful:

let config = AACConfiguration()
config.setValue("Provide feedback", for: .votingFeedbackTitle)
config.setValue("Thumbs up", for: .votingUseful)
config.setValue("Thumbs down", for: .votingNotUseful)

Refreshing a stream container manually

You can choose to manually refresh a stream container or single card view, such as when a push notification arrives while your app is open. Refreshing will result in the stream container or single card view checking for new cards immediately, and showing any that are available.

streamContainer.refresh()

Responding to card events

The SDK allows you to perform custom actions in response to events occurring on a card, such as when a user:

  • submits a card;
  • dismisses a card;
  • snoozes a card;
  • indicates a card is useful (when card voting is enabled);
  • indicates a card is not useful (when card voting is enabled).
  • (introduced in 24.3.0) tries to use the camera but camera usage has been denied or restricted.

To be notified when these happen, assign a card event delegate to your stream container:

// 1. Assign the event delegate
let config = AACConfiguration()
config.cardEventDelegate = self

// 2. Implement the delegate
func streamContainer(_ streamContainerVc: AACStreamContainerViewController, didTriggerCardEvent event: AACCardEvent) {
// Perform a custom action in response to the card event.
}

Sending custom events

You can send custom events directly to the Atomic Platform for the logged in user, via the sendCustomEvent:completionHandler: method on AACSession.

A custom event can be used in the Workbench to create segments for card targeting. For more details of custom events, see Custom Events.

The event will be created for the user defined by the authentication token returned in the session delegate. As such, you cannot specify target user IDs using this method.

// Create the custom event, the properties are optional.
let customEvent = AACCustomEvent(name: "myEvent", properties: ["firstName": "John", "lastName": "Smith"])

AACSession.send(customEvent) { error in
if error != nil {
// Handle error here.
return
}
// Event successfully sent.
}

Error handling

If the error object is non-nil, the error domain will be AACSessionSendCustomEventErrorDomain - look for a specific error code in the AACSessionSendCustomEventErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

Push notifications

To use push notifications in the SDK, you'll need to set up your notification preferences and add your iOS push certificate in the Workbench (see: Notifications in the Configuration area), then request push notification permission in your app.

Once this is done, you can configure push notifications via the SDK.

For a working example of integrating push notifications in a native iOS app, see the notifications branch of our boilerplate app.

The steps below can be configured in any order in your app.

1. Register the user against specific stream containers for push notifications

You need to signal to the Atomic Platform which stream containers are eligible to receive push notifications in your app for the current device:

AACSession.registerStreamContainers(forPushNotifications: ["1"])

You will need to do this each time the logged in user changes.

This method takes an optional parameter - completionHandler. This parameter is a block which takes an NSError as its parameter. It is called when the request completes. If error is nil, the request succeeded, otherwise the request failed.

Error handling

If the error object is non-nil, the error domain will be AACSessionPushRegistrationErrorDomain - look for a specific error code in the AACSessionPushRegistrationErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

AACSession.registerStreamContainers(forPushNotifications: [containerId]) { error in
if let error = error as? NSError {
if let errorCode = AACSessionPushRegistrationError.Code(rawValue: error.code) {
switch errorCode {
case .domainDataError:
// Deal with data error.
let dataError = error.userInfo[NSUnderlyingErrorKey] as? NSError
case .codeNetworkError:
// Deal with network error.
let networkError = error.userInfo[NSUnderlyingErrorKey] as? NSError
@unknown default:
// A new type of error is added in the future.
let unknownError = error.userInfo[NSUnderlyingErrorKey] as? NSError
}
}
}
}

There is another optional version which takes a parameter - notificationsEnabled. This parameter updates the user's notificationsEnabled preference in the Atomic Platform. You can also inspect and update this preference using the Atomic API - consult the Atomic API documentation for user preferences for more information.

If you pass NO for this parameter, the user's notificationsEnabled preference will be set to false, which means that they will not receive notifications on any eligible devices, even if their device is registered in this step, and the device push token is passed to Atomic in the next step. If you pass YES, the user's notificationEnabled preference will be set to true, which is the default, and allows the user to receive notifications. This allows you to explicitly enable or disable notifications for the current user, via UI in your own app - such as a notification settings screen.

If you call registerStreamContainersForPushNotifications:completionHandler:, without supplying the notificationsEnabled parameter, the user's notificationsEnabled preference in the Atomic Platform is not affected.

AACSession.registerStreamContainers(forPushNotifications: ["1"], notificationsEnabled: false)

To deregister the device for Atomic notifications for your app, such as when a user completely logs out of your app, call deregisterDeviceForNotifications: on AACSession. If the de-registration fails, the error object will be populated, the error domain will be AACSessionPushRegistrationErrorDomain - look for a specific error code in the AACSessionPushRegistrationErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

AACSession.deregisterDeviceForNotifications { error in
if let error = error as? NSError {
if let errorCode = AACSessionPushRegistrationError.Code(rawValue: error.code) {
switch errorCode {
case .domainDataError:
// Deal with data error.
let dataError = error.userInfo[NSUnderlyingErrorKey] as? NSError
case .codeNetworkError:
// Deal with network error.
let networkError = error.userInfo[NSUnderlyingErrorKey] as? NSError
@unknown default:
// A new type of error is added in the future.
let unknownError = error.userInfo[NSUnderlyingErrorKey] as? NSError
}
}
}
}

2. Send the push token to the Atomic Platform

Send the device's push token to the Atomic platform when it changes, by calling:

AACSession.registerDevice(forNotifications:completionHandler:)

in your app delegate's application:didRegisterForRemoteNotificationsWithDeviceToken: method.

You can also call this SDK method any time you want to update the push token stored for the user in the Atomic Platform; pass the method the NSData instance representing the push token. The token is automatically converted to a hex string by the SDK.

You will also need to update this token every time the logged in user changes in your app, so the Atomic Platform knows who to send notifications to.

This method takes an optional parameter - completionHandler. This parameter is a block which takes an NSError as its parameter. It is called when the request completes. If error is nil, the request succeeded, otherwise the request failed.

The returned error has an explicit type. The error domain will be AACSessionPushRegistrationErrorDomain - look for a specific error code in the AACSessionPushRegistrationErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

AACSession.registerDevice(forNotifications: Data()) { error in
if let error = error as? NSError {
if let errorCode = AACSessionPushRegistrationError.Code(rawValue: error.code) {
switch errorCode {
case .domainDataError:
// Deal with data error.
let dataError = error.userInfo[NSUnderlyingErrorKey] as? NSError
case .codeNetworkError:
// Deal with network error.
let networkError = error.userInfo[NSUnderlyingErrorKey] as? NSError
@unknown default:
// A new type of error is added in the future.
let unknownError = error.userInfo[NSUnderlyingErrorKey] as? NSError
}
}
}
}

3. (Optional) Perform a custom action when tapping on a push notification

When a user taps on a push notification delivered to your app, you can ask the SDK whether the push notification payload originated from Atomic. If so, you will be provided with a structured object that you can inspect, to perform custom actions based on the payload. The custom data for the notification, that was sent with the original event to Atomic, is exposed by the SDK through the detail property. This detail property shows the value of what was sent in the notificationDetail object as part of the API request payload.

Read more about the shape of this payload in the Sending custom data in push notification payloads guide.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == UNNotificationDefaultActionIdentifier, let notification = AACSession.notification(fromPushPayload: response.notification.request.content.userInfo) {
// The payload originated from Atomic - use the properties on the object to determine the action to take.
}

completionHandler()
}

4. (Optional) Track when push notifications are received

To track when push notifications are delivered to your user's device, you can use a Notification Service Extension.

While the Atomic SDK does not supply this extension, it does supply a method you can call within your own extension to track delivery.

From within your notification service extension's didReceiveNotificationRequest:withContentHandler: method, call the + [AACSession trackPushNotificationReceived:completionHandler:] method to track the delivery of a push notification. You must supply the push notification payload provided to your extension (stored in request.content.userInfo), as well as a completion handler.

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {

// It is recommended to use the login method to initialize the SDK.
AACSession.login(withEnvironmentId: "<environmentId>", apiKey: "<apiKey>", sessionDelegate: <Your session delegate>, apiBaseUrl: <API base URL>)

AACSession.trackPushNotificationReceived(request.content.userInfo) { (error) in
contentHandler(request.content)
}
}

This delivery event appears in your card's analytics as a notification-received event, with a timestamp indicating when the event was generated, as well as the card instance ID and stream container ID for the card.

Error handling

If the error object is not nil, the error domain will be AACSessionPushTrackingErrorDomain.

Error code AACSessionPushTrackingErrorCodeInvalidPayload indicates the payload passed to the method is invalid or does not represent an Atomic push notification.

Error code AACSessionPushTrackingErrorCodeFailedToSend indicates the event, tracking push notification delivery, could not be sent to the Atomic Platform. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

Troubleshooting push notifications

If you run into issues setting up push notifications, take a look at the Troubleshooting section in the iOS boilerplate app repo.

Retrieving card count

Use user metrics

It is recommended that you use user metrics to retrieve the card count instead. See the next section for more information.

The SDK supports observing the card count for a particular stream container, or receiving a single card count, even when that stream container does not exist in memory.

// Observe the card count
AACSession.observeCardCountForStreamContainer(withIdentifier: "1", interval: 15) { count in
if let count = count {
print("There are \(count) cards in the container.")
}
}

// Retrieve a one-off card count
AACSession.requestCardCountForStreamContainer(withIdentifier: "1") { count in
if let count = count {
print("There are \(count) cards in the container.")
}
}

If you choose to observe the card count, by default it is updated immediately after the published card number changes. If for some reason the WebSocket is not available, the count is then updated periodically at the interval you specify. When the card count changes, the handler block is called with the new card count, or nil if the card count could not be fetched. The time interval cannot be smaller than 1 second.

When you want to stop observing the card count, you can remove the observer using the token returned from the observation call above:

AACSession.stopObservingCardCount(token)

When a stream container is present on screen, or when the card count is requested, a notification is posted (AACSessionCardCountDidChange) every time the visible card count changes, such as when a card is dismissed or completed. You can observe this notification to get the latest card count:

NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: AACSessionCardCountDidChange), object: nil, queue: nil) { (notification) in
if let userInfo = notification.userInfo,
let visibleCards = userInfo[AACSessionCardCountUserInfoKey],
let streamContainerId = notification.object {
print("*** There are \(visibleCards) visible cards in stream container \(streamContainerId).")
}
}

The count of visible cards is available via the AACSessionCardCountUserInfoKey key in the userInfo dictionary, and the notification's object represents the stream container ID. You can listen for the card count of only a particular stream container by specifying that stream container ID in the object parameter when adding an observer.

If you want to retrieve the total number of cards in the container (rather than the number visible), use the AACSessionTotalCardCountUserInfoKey key in the userInfo dictionary.

When not in single card view, AACSessionCardCountUserInfoKey and AACSessionTotalCardCountUserInfoKey report the same value.

Retrieving the count of active and unseen cards

What is an active card? What is an unseen card?

All cards are unseen the moment they are sent. A card becomes "seen" when it has been shown on the customer's screen (even if only briefly or partly). A quick scroll-through might not make the card "seen", this depends on the scrolling speed. The user metrics only count "active" cards, which means that snoozed and embargoed cards will not be included in the count.

The Atomic iOS SDK exposes a new object: user metrics. These metrics include:

  • The number of cards available to the user across all stream containers;
  • The number of cards that haven't been seen across all stream containers;
  • The number of cards available to the user in a specific stream container (equivalent to the card count functionality in the previous section);
  • The number of cards not yet seen by the user in a specific stream container.

These metrics enable you to display badges in your UI that indicate how many cards are available to the user but not yet viewed, or the total number of cards available to the user.

This method returns nil if user metrics cannot be retrieved.

AACSession.userMetrics { metrics, error in
if let userMetrics = metrics {
print("Total cards across all containers: \(userMetrics.totalCards())")
print("Total cards across a specific container: \(userMetrics.totalCardsForStreamContainer(withId: "containerId"))")

print("Unseen cards across all containers: \(userMetrics.unseenCards())")
print("Unseen cards across a specific container: \(userMetrics.unseenCardsForStreamContainer(withId: "containerId"))")
}
}

Error handling

If the error object is non-nil, the error domain will be AACSessionUserMetricsErrorDomain - look for a specific error code in the AACSessionUserMetricsErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

Runtime variables

Runtime variables are resolved in the SDK at runtime, rather than from an event payload when the card is assembled. Runtime variables are defined in the Atomic Workbench.

The SDK will ask the host app to resolve runtime variables when a list of cards is loaded (and at least one card has a runtime variable), or when new cards become available due to WebSockets pushing or HTTP polling (and at least one card has a runtime variable).

Runtime variables are resolved by your app via the -cardSessionDidRequestRuntimeVariables:completionHandler: method on AACRuntimeVariableDelegate. If this method is not implemented on AACRuntimeVariableDelegate, runtime variables will fall back to their default values, as defined in the Atomic Workbench. To resolve runtime variables, you pass an object conforming to AACRuntimeVariableDelegate to AACConfiguration.runtimeVariableDelegate when creating a stream container or a single card view.

Only string values

Runtime variables can currently only be resolved to string values.

The -cardSessionDidRequestRuntimeVariables:completionHandler: method, when called by the SDK, provides you with:

  • An array of objects representing the cards in the list. Each card object contains:
    • The event name that triggered the card’s creation;
    • The lifecycle identifier associated with the card;
    • A method that you call to resolve each variable on that card (-resolveRuntimeVariableWithName:value:).
  • A block callback (completionHandler) which must be called by the host app, with the resolved cards, once all variables are resolved.

If a variable is not resolved, that variable will use its default value, as defined in the Atomic Workbench.

If you do not call the completionHandler before the runtimeVariableResolutionTimeout elapses (defined on AACConfiguration), the default values for all runtime variables will be used. Calling the completion handler more than once has no effect.

func cardSessionDidRequestRuntimeVariables(_ cardsToResolve: [AACCardInstance], completionHandler: @escaping AACSessionRuntimeVariablesHandler) {
for card in cardsToResolve {
// Resolve variables on all cards.
// You can also inspect `lifecycleId` and `cardInstanceId` to determine what type of card this is.
card.resolveRuntimeVariable(withName: "numberOfItems", value: "12")
}

completionHandler(cardsToResolve)
}

Updating runtime variables manually

You can manually update runtime variables at any time by calling the updateVariables method on AACStreamContainerViewController or AACSingleCardView:

streamVc.updateVariables()

Accessibility and fonts

The Atomic SDKs support a variety of accessibility features on each platform. These features make it easier for vision-impaired customers to use Atomic's SDKs inside of your app.

These features also allow your app, with Atomic integrated, to continue to fulfil your wider accessibility requirements.

Dynamic Type

The Atomic iOS SDK supports dynamic font scaling. On iOS this feature is called Dynamic Type.

To opt-in to Dynamic Type for a typography style:

  1. Open the Atomic Workbench;
  2. Navigate to Configuration > SDK > Container themes. Select the theme you want to edit.
  3. When editing a typography style in the theme (e.g. Headline > Typography), turn Dynamic scaling on.
  4. Optionally specify a minimum and maximum font size to use when scaling is applied.

The SDK automatically scales the typography style from the base font size that you specify, adding or subtracting a pixel value from this base font size, to create a size tailored to each Dynamic Type level. The minimum font size is 1px - this is enforced by the SDK.

The pixel value that is added or subtracted for each Dynamic Type level is determined inside the SDK and is not customizable.

info

Typography styles that do not have 'Dynamic scaling' enabled stay at a fixed size regardless of the user's text size (this is the default behavior for all typography styles).

Font sizing behavior

When the user changes their system-wide text size on iOS, the SDK will automatically re-render any components containing typography styles that opt-in to dynamic scaling (typography styles that have the 'Dynamic scaling' option enabled in the stream container theme).

  • On iOS, the font size in the typography style has a delta value added to, or subtracted from, it using the following values. User-selected sizes correspond to the values defined in UIContentSizeCategory:
User-selected sizeDelta applied
XS-3
S-2
M-1
L (default)0
XL2
XXL4
XXXL6
Accessibility M10
Accessibility L15
Accessibility XL20
Accessibility XXL25
Accessibility XXXL30

Once these delta values (iOS) are applied, the minimum and maximum scaling sizes are applied to the resultant font size. See the example below for a demonstration of how this works.

Example scaling scenario

A typography style is configured in the Workbench with the following properties:

  • Font size: 17px
  • Dynamic scaling: On
  • Dynamic scaling minimum size: 15px
  • Dynamic scaling maximum size: 30px

On iOS, the following font sizes would be used for each content size category:

User-selected sizeResultant size
XS15px (14px is below min)
S15px (at or above min)
M16px
L (default)17px
XL19px
XXL21px
XXXL23px
Accessibility M27px
Accessibility L30px (32px is above max)
Accessibility XL30px (37px is above max)
Accessibility XXL30px (42px is above max)
Accessibility XXXL30px (47px is above max)

Using embedded fonts in themes

When creating your stream container's theme in the Atomic Workbench, you optionally define custom fonts that can be used by the stream container for UI elements. When defined in the Workbench, these fonts must point to a remote URL, so that the SDK can download the font, register it against the system and use it.

It is likely that the custom fonts you wish to use are already part of your app, particularly if they are a critical component of your brand identity. If this is the case, you can have a stream container font - with a given font family name, weight and style - reference a font embedded in your app instead. This is also useful if the license for your font only permits you to embed the font and not download it from a remote URL.

To map a font in a stream container theme to one embedded in your app, use the registerEmbeddedFonts: method on AACSession, passing an array of AACEmbeddedFont objects, each containing the following:

  • A familyName that matches the font family name declared in the Atomic Workbench;
  • A weight - a value of AACFontWeight, also matching the value declared in the Atomic Workbench;
  • A style - either italic or regular;
  • A postscriptName, which matches the Postscript name of a font available to your app. This can be a font bundled with your application or one provided by the operating system.

If the familyName, weight and style of a font in the stream container theme matches an AACEmbeddedFont instance that you've registered with the SDK, the SDK will use the postscriptName to create an instance of your embedded font, and will not download the font from a remote URL.

In the example below, any use of a custom font named BrandFont in your theme, that is bold and italicized, would use the embedded font named HelveticaNeue instead:

Invalid Postscript names

If the Postscript name provided is invalid, or the family name, weight and style do not match a custom font in the stream container theme exactly, the SDK will download the font at the remote URL specified in the theme instead.

AACSession.register([
AACEmbeddedFont(familyName: "BrandFont", postscriptName: "HelveticaNeue", weight: .weightBold, style: .italic)
])

SDK Analytics

Default behavior

The default behavior is to not send analytics for resolved runtime variables. Therefore, you must explicitly enable this feature to use it.

If you use runtime variables on a card, you can optionally choose to send the resolved values of any runtime variables back to the Atomic Platform as an analytics event. This per-card analytics event - runtime-vars-updated - contains the values of runtime variables rendered in the card and seen by the end user. Therefore, you should not enable this feature if your runtime variables contain sensitive data that you do not wish to store on the Atomic Platform.

To enable this feature, set the runtimeVariableAnalytics flag on your configuration's features object:

let config = AACConfiguration()
config.features.runtimeVariableAnalytics = true

Network request security

The Atomic iOS SDK provides functionality to further secure the SDK implementation in your own app.

Allow or deny network requests

You can choose to implement a request delegate, which determines whether requests originating from the Atomic SDK are allowed to proceed. This enables you to permit network requests only to domains or subdomains that you approve.

The request delegate is called before every network request in the SDK, and during SSL certificate validation.

To enable this functionality, create a request delegate conforming to the AACRequestDelegate protocol, implement the dispositionForAtomicRequest: method and call setRequestDelegate on AACSession to assign it to the SDK. The request delegate is used for every network request from the SDK.

Within the dispositionForAtomicRequest: method, you can inspect the request URL and return one of the following dispositions:

  • allow: The request is allowed to proceed.
  • deny: The request is not allowed to proceed and will be cancelled.
  • allowWithCertificatePins: The request can proceed if a hash of the subject public key info (SPKI) from part of the certificate chain matches one of the pin objects provided (see below for more information).

All of these dispositions are available as static methods on AACRequestDisposition.

If you do not implement this request delegate, all requests are permitted. Specifically, if you deny the request for connecting to the WebSockets server, the SDK will try to use HTTP polling instead.

WebSockets requests

The URL of WebSockets follows a pattern like wss://[url]/socket. If the allowWithCertificatePins disposition is used with this URL, the request will be denied.

Allow requests with certificate pinning

When you return allowWithCertificatePins in the AACRequestDelegate above, you will be required to supply a set of AACCertificatePin objects. Each object contains a SHA-256 hash of the subject public key info (SPKI) from part of the domain's certificate chain, which is then base64 encoded.

Pin requests to all available Amazon root certificate

When pinning requests for the Atomic API (<orgId>.client-api.atomic.io), we strongly recommend pinning to all available Amazon root certificates, which is the approach recommended by Amazon Web Services. SHA256 hashes for Amazon's root certificates are available on the Amazon Trust website.

You can convert these SHA256 hashes to a base64 representation, as required by the SDK, using the following Terminal command: openssl base64 -e <<< "<SHA256 hash>"

If the hashed, base64-encoded SPKI from part of the request's certificate chain matches any of the provided pin objects, the request is allowed to proceed. If it does not match any of the provided pins, the request is denied.

If the allowWithCertificatePins disposition is used with a non-HTTPS URL, the request will be denied.

func disposition(forAtomicRequest requestUrl: URL) -> AACRequestDisposition {
guard let host = requestUrl.host else {
// Deny requests without a host.
return .deny()
}

switch(host) {
case "atomic.io":
// Allow requests to atomic.io with certificate pinning.
let pins = Set([
AACCertificatePin(sha256Hash: "AAAAAA=")
])
return .allow(with: pins)
case "placeholder.com":
// Always allow requests to placeholder.com.
return .allow()
default:
// Deny all other requests.
return .deny()
}
}

AACSession.setRequestDelegate(self)

Updating user data

The SDK allows you to update profile & preference data for the logged in user via the updateUser method. This is the user as identified by the auth token provided by the authentication callback.

Setting up profile fields

For simple setup, create an AACUserSettings object and set some profile fields, then call method +[AACSession updateUser:completionHandler:]. The following optional profile fields can be supplied to update the data for the user. Any fields which have not been supplied will remain unmodified after the user update.

  • external_id: An optional string that represents an external identifier for the user.
  • name: An optional string that represents the name of the user.
  • email: An optional string that represents the email address of the user.
  • phone: An optional string that represents the phone number of the user.
  • city: An optional string that represents the city of the user.
  • country: An optional string that represents the country of the user.
  • region: An optional string that represents the region of the user.

The following code snippet shows how to set up some profile fields.

let settings = AACUserSettings()
// Set up some basic profile fields.
settings.externalID = "an external ID"
settings.name = "John Smith"

AACSession.updateUser(settings)

Setting up custom profile fields

You can also setup your custom fields of the user profile. Custom fields must first be created in Atomic Workbench before updating them. For more details of custom fields, see Custom Fields.

There are two types of custom fields: date and text. Use -[AACUserSettings setDate:forCustomField:] for date fields and -[AACUserSettings setText:forCustomField:] for text fields.

Note: Use the name property in the Workbench to identify a custom field, not the label property.

The following code snippet shows how to set up a date and a text field.

settings.setDate(Date(timeIntervalSinceNow: 0), forCustomField: "custom_date_field")
settings.setText("some custom texts", forCustomField: "custom_text_field")

Setting up notification preferences

You can use the following optional property and method to update the notification preferences for the user. Again, any fields which have not been supplied will remain unmodified after the user update.

  • notificationsEnabled: An optional boolean to determine whether notifications are enabled. The default notification setting of a user is YES.
  • setNotificationTime:weekday: An optional method that defines the notification time preferences of the user for different days of the week. If you specify AACUserNotificationTimeframeWeekdaysDefault to the weekday parameter, the notification time preferences will be applied to every day.

Each day accepts an array of notification time periods, these are periods during which notifications are allowed. If an empty array is provided notifications will be disabled for that day.

The following code snippet shows how to set up notification periods between 8am - 5:30pm & 7pm - 10pm on Monday.

let settings = AACUserSettings()
let timeframes = [AACUserNotificationTimeframe(startHour: 8,
startMinute: 0,
endHour: 17,
endMinute: 30),
AACUserNotificationTimeframe(startHour: 19,
startMinute: 0,
endHour: 22,
endMinute: 0)]

settings.setNotificationTime(timeframes, weekday: .monday)

Hours are in the 24h format that must be between 0 & 23 inclusive, while minutes are values between 0 & 59 inclusive.

UpdateUser method: full example

The following code snippet shows an example of using the updateUser method to update profile fields, custom profile fields and notification preferences.

let settings = AACUserSettings()
settings.externalID = "123"
settings.name = "User Name"
settings.email = "email@example.com"
settings.phone = "(+64)210001234"
settings.city = "Wellington"
settings.country = "New Zealand"
settings.region = "Wellington"
// Any further custom fields that have already been defined in the Workbench.
settings.setDate(Date(timeIntervalSinceNow: 0), forCustomField: "myCustomDateField")
settings.setText("My custom value", forCustomField: "myCustomTextField")

settings.notificationsEnabled = true

// Set up notification timeframes from 08:30 to 17:45, on all work days.
settings.setNotificationTime([AACUserNotificationTimeframe(startHour: 8,
startMinute: 0,
endHour: 17,
endMinute: 45)],
weekday: .default)
settings.setNotificationTime([], weekday: .saturday)
settings.setNotificationTime([], weekday: .sunday)
AACSession.updateUser(settings) { error in
if let error = error as? NSError {
if let errorCode = AACSessionUserMetricsError.Code(rawValue: error.code) {
switch errorCode {
case .dataError:
// Deal with data error.
let dataError = error.userInfo[NSUnderlyingErrorKey] as? NSError
case .networkError:
// Deal with network error.
let networkError = error.userInfo[NSUnderlyingErrorKey] as? NSError
@unknown default:
// A new type of error is added in the future.
let unknownError = error.userInfo[NSUnderlyingErrorKey] as? NSError
}
}
}
}
Optional values

Though all fields of AACUserSettings are optional, you must supply at least one field when calling +[AACSession updateUser:completionHandler:].

Error handling

If the error object is non-nil, the error domain will be AACSessionUpdateUserErrorDomain - look for a specific error code in the AACSessionUpdateUserErrorCode enumeration to determine the cause of the error. NSUnderlyingErrorKey will also be populated in the error's userInfo dictionary.

Observing SDK events

The Atomic iOS SDK provides functionality to observe SDK events that symbolize identifiable SDK activities such as card feed changes or user interactions with cards. The following code snippet shows how to observe SDK events.

AACSession.observeSDKEvents { event in
// Do something with the event.
}
info

Only one SDK event observer can be active at a time. If you call this method again, it will replace the previous observer.

The SDK provides all observed events in the base class AACSDKEvent. To access more specific information about a particular event, you can cast it to its corresponding event class. The table below lists all currently supported events, including some that are part of Atomic analytics. For detailed information about these events, please refer to Analytics reference.

Event nameEvent classAnalyticsDescription
card-dismissedAACSDKEventCardDismissedYESThe user dismisses a card.
card-snoozedAACSDKEventCardSnoozedYESThe user snoozes a card.
card-completedAACSDKEventCardCompletedYESThe user submits a card.
card-feed-updatedAACSDKEventCardFeedUpdatedNOA card feed has been updated
card-displayedAACSDKEventCardDisplayedYESA card is displayed in a container.
card-voted-upAACSDKEventCardVotedUpYESThe user taps on the “This is useful” option.
card-voted-downAACSDKEventCardVotedDownYESThe user taps the “Submit” button on the card feedback screen.
runtime-vars-updatedAACSDKEventRuntimeVarsUpdatedYESOne or more runtime variables are resolved.
stream-displayedAACSDKEventStreamDisplayedYESA stream container is first loaded or returned to.
user-redirectedAACSDKEventUserRedirectedYESThe user is redirected by a URL or a custom payload.
snooze-options-displayedAACSDKEventSnoozeOptionsDisplayedYESThe snooze date/time selection UI is displayed.
snooze-options-canceledAACSDKEventSnoozeOptionsCanceledYESThe user taps the “Cancel” button in the snooze UI.
card-subview-displayedAACSDKEventCardSubviewDisplayedYESA subview of card is opened.
card-subview-exitedAACSDKEventCardSubviewExitedYESThe user leaves the subview.
video-playedAACSDKEventVideoPlayedYESThe user hits the play button of a video.
video-completedAACSDKEventVideoCompletedYESA video finishes playing.
sdk-initializedAACSDKEventSDKInitializedYESAn instance of the SDK is initialized, or the JWT is refreshed.
request-failedAACSDKEventRequestFailedYESAny API request to the Atomic client API fails within the SDK, or a failure in WebSocket causes a fallback to HTTP polling.
Note: Network failure and request timeout does not trigger this event.
notification-receivedAACSDKEventNotificationReceivedYESA push notification is received by the SDK.
user-file-uploads-startedAACSDKEventUserFileUploadsStartedYESThe user has started the process of uploading files for a card, which is normally part of a card submission.
user-file-uploads-completedAACSDKEventUserFileUploadsCompletedYESThe user has successfully completed the process of uploading files for a card.
user-file-uploads-failedAACSDKEventUserFileUploadsFailedYESThe user has completed the process of uploading files, with one or more uploads failing.

Accessing a specific SDK event

To access the unique properties of each event, you need to cast the base class AACSDKEvent passed in the callback to the specific event classes. You can determine the type of the event using either the eventType or eventName property. It is recommended to use eventType as it benefits from the code-completion system.

The following code snippet shows how to retrieve the attributes of a card's subview when its being displayed.

AACSession.observeSDKEvents { event in
switch event.eventType {
case .cardSubviewDisplayed:
if let event = event as? AACSDKEventCardSubviewDisplayed {
print("The subview \(event.subviewTitle) of card \(event.cardInstanceId) has been displayed.")
}
default:
break
}
}

Accessing the raw contents

You can access the raw value of an event by calling the method getRawContents on the SDK event object. It will return a NSDictionary object containing all the data available to this event. Raw contents are only for debugging purposes and are subject to change. Please do not rely on it in production code.

AACSession.observeSDKEvents { event in
let rawData = event.getRawContents()
// Inspect the raw data for more details.
}

Stopping the observation

To stop the observation, call either [AACSession stopObservingSDKEvents] or [AACSession observeSDKEventsWithCompletionHandler:nil].

Metadata pass-through

Any metadata sent from an API request can now be accessed from within the SDK using the SDK Event Observer, once enabled in the Workbench

info

Currently the metadata is only returned in the card-displayed event.

An example body in an API request to an Action Flow trigger endpoint might look like:

{
"flows": [
{
"payload": {
"metadata": {
"account" : "123445",
"fruit" : "apple"
}
},
"target": {
"type": "user",
"targetUserIds": "user-123"
}
}
]
}

To retrieve the metadata just capture the card-displayed event in an SDK Event Observer, such as:

extension MyViewController: ACSSDKEventManagerDelegate {
func acsSDKEventTriggered(_ sdkEvent: AACSDKEvent) {
switch sdkEvent.eventType {
case .cardDisplayed:
let eventPayload = (sdkEvent as! AACSDKEventCardDisplayed).payloadMetadata
print(eventPayload)
default:
break
}
}
}

Any metadata will be populated in the payloadMetadata dictionary property of AACSDKEvent.

More examples

Fetching unseen card number in realtime

When your application will display the number of unseen cards on the app icon, it is crucial to ensure that this number stays current as the user navigates through cards. This way, when they return to the home screen, they see an up to date count of unseen cards. To make this possible, we must fetch the count of unseen cards in real time.

You can obtain the count of unseen cards from user metrics. However, since this is a singular call, we need to invoke this method repeatedly to keep the count current. By monitoring SDK events, we can update the unseen card count every time a card's viewed status changes. The code snippet below shows how to fetch the number of unseen cards for a container under these conditions.

AACSession.observeSDKEvents { event in
switch event.eventType {
case .cardDisplayed, .cardFeedUpdated:
AACSession.userMetrics { metrics, error in
if let metrics = metrics {
let containerId = "1234"
let unseenCardsForContainer = metrics.unseenCardsForStreamContainer(withId: containerId)
print("The unseen cards for container \(containerId) is \(unseenCardsForContainer)")
}
}
default:
break
}
}

Capturing the voting-down event

The following code snippet shows how to capture an event when the user votes down for a card.

AACSession.observeSDKEvents { event in
switch event.eventType {
case .cardVotedDown:
if let event = event as? AACSDKEventCardVotedDown {
print("The user has voted down for the card \(event.cardInstanceId)")
switch event.reason {
case .notRelevant:
print("The reason is it's not relevant.")
case .tooOften:
print("The reason is it's displayed too often.")
case .other:
print("The user provided some other reasons.")
if let message = event.otherMessage {
print("Other reason: \(message)")
}
@unknown default:
print("The reason is unknown.")
}
}
default:
break
}
}

Image linking

(introduced in 24.1.0)

You can now use images for navigation purposes, such as directing to a web page, opening a subview, or sending a custom payload into the app, as if they were buttons. This functionality is accessible in Workbench, where you can assign custom actions to images on your cards.

The updated analytics event 'user-redirected'

Redirection initiated by images also trigger the user-redirected analytics event. To accurately identify the origin of this event, a new detail property has been added, with four distinct values:

  • image: The event was activated by an image.
  • submitButton: The redirection was initiated via a submit button.
  • linkButton: A link button was the source of the event.
  • textLink: The trigger was a link embedded within markdown text.

See Analytics or Analytics reference for more details of the event user-redirected.

In iOS SDK, you can also capture the new detail property via SDK event observer. The following code snippet shows how to parse this property.

AACSession.observeSDKEvents { event in
switch event.eventType {
case .userRedirected:
if let userRedirectedEvent = event as? AACSDKEventUserRedirected {
switch userRedirectedEvent.detail {
case .image:
print("Event triggered by an image.")
case .linkButton:
print("Event triggered by a link button.")
case .submitButton:
print("Event triggered by a submit button.")
case .textLink:
print("Event triggered by a markdown link text.")
@unknown default:
print("Event triggered by an unknown component.")
}
}
default:
break
}
}

See the SDK event observer for more details on the event observer feature.

Capture image-triggered custom payload

In the Atomic Workbench, the functionality for custom payload has expanded. Initially, you could create a submit or link button with a custom action payload. Now, this capability extends to images, allowing the use of an image with a custom payload to achieve similar interactive outcomes as you would with buttons.

When such an image is tapped, the streamContainerDidTapLinkButton:withAction: method is called on your action delegate.

Unified Handling Approach for Images and Link Buttons

In this scenario, an image is treated similarly to a link button, meaning the same delegate method used for link buttons is applied to images as well.

This approach streamlines the handling of user interactions with both elements, ensuring a concise behavior across the UI.

The second parameter to this method is an action object, containing the payload that was defined in the Workbench for that button. You can use this payload to determine the action to take, within your app, when the image is tapped.

The action object also contains the card instance ID and stream container ID where the custom action was triggered.

The following code snippet navigates the user to the home screen upon receiving a specific payload.

// 1. Assign the action delegate
let config = AACConfiguration()
config.actionDelegate = self

// 2. Implement the callbacks
func streamContainerDidTapLinkButton(_ streamContainer: AACStreamContainerViewController, with action: AACCardCustomAction) {
if let screenName = action.actionPayload["screen"] as? String, screenName == "home-screen" {
// Perform an action
self.navigateToHomeScreen()
}
}

Custom Icons

(Introduced in 24.2.0)

This is currently a beta feature in Workbench.

Requires iOS 13+

See the card element reference for supported SVG features in custom icons.

The SDK now supports the use of custom icons in card elements. When you are editing a card in the card template editor of the Workbench you will notice that for card elements that support it the properties panel will show an "Include icon" option. From this location you can select an icon to use, either from the Media Library or Font Awesome.

Choosing to use an icon from the Media Library you have the ability to provide an SVG format icon and an optional fallback icon to be used in case the SVG fails to load. The "Select icon" dropdown will present any SVG format assets in your media library which can be used as a custom icon. To add an icon for use you can press the "Open Media Library" button at the bottom of the dropdown.

Custom icon colors

The Workbench theme editor now provides the ability to set a color and opacity value for icons in each of the places where an icon may be used. The SDK will apply the following rules when determining what color the provided icon should be displayed in:

  • All icons will be displayed with the colors as dictated in the SVG file.
  • Black is used if no colors are specified in the SVG file.
  • Where a currentColor value is used in the SVG file, the following hierarchy is applied:
    1. Use the icon theme color if this has been supplied.
    2. Use the color of the text associated with the icon.
    3. Use a default black color.

Custom icon sizing

The custom icon will be rendered in a square icon container with a width & height in pixels equal to the font size of the associated text. Your supplied SVG icon will be rendered centered inside this icon container, at its true size until it is constrained by the size of container, at which point it will scale down to fit.

Fallback Rules

There are two scenarios where a fallback could occur for an SVG icon:

  1. If the provided SVG image is inaccessible due to a broken URL or network issues, such as those caused by certificate pinning.
  2. If the SVG icon is not supported on iOS. Currently, SVG features are not fully supported in the iOS SDK, so please check with our support team for details on supported SVG images.

In these scenarios, the following fallback rules apply:

  1. The fallback FontAwesome icon is used if it is set in Atomic Workbench for this custom icon.
  2. Otherwise, a default placeholder image is displayed.

Multiple display heights

(Introduced in 24.2.0)

You can now specify different display heights in Atomic Workbench for banner and inline media components. There are four options, each of which defines how the thumbnail cover of that media is displayed.

  • Tall The thumbnail cover is 200 display points high, spanning the whole width and cropped as necessary. This is the default value and matches existing media components.
  • Medium The same as "Tall", but only 120 display points high.
  • Short The same as "Tall" but 50 display points high. Not supported for inline or banner videos.
  • Original The thumbnail cover will maintain its original aspect ratio, adjusting the height dynamically based on the width of the card to avoid any cropping.

Note: For older versions of the SDK, all options will fall back to "Tall".

File Upload

(introduced in 24.3.0)

This is currently a beta feature in Workbench.

The File Upload element allows you to add a component to a card through which users can upload a file. Currently, only images can be selected. Multiple updates in the SDK support its customization.

File type restrictions

Currently, only static images can be uploaded; GIFs and non-image file types are not supported. Images in formats other than JPEG and PNG are converted to JPEG before uploading. For Live Photos, only their key photos will be uploaded.

Preparing to Use File Upload

The File Upload element enables users to upload a picture taken from the camera or selected from the photo library. However, you must set up an NSCameraUsageDescription key in the host app's Info.plist, with a string value explaining to the user how the app uses this data. The app will crash if that key isn't set and the user taps "Take Photo" in the photo pick menu of File Upload element. See Apple's documentation for more details.

Configuring Camera Access Request Toast

If the user initially denies the camera access request, the iOS's built-in request dialog won't appear again due to iOS behaviour. By default, when the user tries to take a photo under these circumstances, Atomic SDK will show a toast message at the bottom, prompting them to enable camera access. The user can tap a "Settings" button on the toast to go to the host app's settings page to turn on the camera. Both the prompting message and button title can be customized, see Custom Strings for more details.

In the AACConfiguration object, a new bitmask value AACUIElementRequestCameraUsage is supported by the enabledUiElements property. The toast will display if you either leave this property unset or explicitly include the AACUIElementRequestCameraUsage bitmask in it. The toast will not display if you do not include this bitmask value in the property.

The following code snippet shows how to enable the camera usage request toast.

let config = AACConfiguration()
// Combine AACUIElementRequestCameraUsage with other options.
config.enabledUiElements = [.requestCameraUsage, .cardListToast]

// Or do not assign values to 'enabledUiElements' at all.

The following code snippet shows how to disable the camera usage request toast.

let config = AACConfiguration()
// Do not include AACUIElementRequestCameraUsage.
config.enabledUiElements = [.cardListHeader, .cardListToast]

Custom Strings

The configuration object also allows you to specify custom strings for messages and button titles using the setValue:forCustomString: method:

  • AACCustomStringProcessingStateMessage: The message displayed on the upload processing overlay during file upload. Defaults to "Sending, please wait...".
  • AACCustomStringProcessingStateCancelButtonTitle: The text displayed on the cancel button in the upload overlay during file upload. Defaults to "Cancel process".
  • AACCustomStringToastFileUploadFailedMessage: Customised toast message shown when file(s) fail to upload during card submission. Defaults to "Couldn't upload file(s)".
  • AACCustomStringRequestCameraAccessMessage: Customised toast message shown when requesting camera access from the user. Defaults to "Access to your camera is required to take photos. Please enable camera access in your device settings".
  • AACCustomStringRequestCameraAccessSettingsTitle: The title for the button in the toast message prompting for camera access, which navigates to the Settings app. Defaults to Settings.

Container Events

The SDK allows you to perform custom actions in response to denied camera usage events. To be notified when these occur, assign a card event delegate to your stream container.

When the user tries to take photo but the camera isn't available, the following two events could occur:

  • AACCardEventKindCameraDenied: The user has explicitly denied an app permission to capture media. Equivalent to iOS's AVAuthorizationStatusDenied status.
  • AACCardEventKindCameraRestricted: The app isn’t permitted to use media capture devices. Equivalent to iOS's AVAuthorizationStatusRestricted status.

The following code snippet shows how to capture these events in the host app.

// 1. Assign the event delegate
let config = AACConfiguration()
config.cardEventDelegate = self

// 2. Implement the delegate
func streamContainer(_ streamContainerVc: AACStreamContainerViewController, didTriggerCardEvent event: AACCardEvent) {
// Perform a custom action in response to the container event.
switch event.kind {
case .cameraDenied, .cameraRestricted:
// Show your own camera access request UI.
default:
break
}
}

SDK Events

There are three new event classes related to file upload: AACSDKEventUserFileUploadsStarted, AACSDKEventUserFileUploadsFailed, and AACSDKEventUserFileUploadsCompleted, each containing an array of uploaded file info.

See Observing SDK events for more details of SDK event observer.

The following code snippet captures the started event and prints out the number of files.

AACSession.observeSDKEvents { event in
switch event.eventType {
case .userFileUploadsStarted:
if let startedEvent = event as? AACSDKEventUserFileUploadsStarted {
print("Captured file upload event \(startedEvent.eventName), with \(startedEvent.fileInfo.count) files")
}
default:
break
}
}
Upload cancellation events

Currently, no events are generated when a user cancels an upload. We will add a corresponding SDK event in upcoming versions.

Utility methods

Debug logging

Debug logging allows you to view more verbose logs regarding events that happen in the SDK. It is turned off by default, and should not be enabled in release builds.

The following code snippet shows how to enable debug logging.

AACSession.enableDebugMode(level)

The parameter level is an integer that indicates the verbosity level of the logs exposed:

  • Level 0: Default, no logs exposed.
  • Level 1: Operations and transactions are exposed.
  • Level 2: Operations, transactions and their details are exposed, plus level 1.
  • Level 3: Expose all logs.

Purging cached data

Purging cached data is described within the Setup section - see Logging out the current user for more details.

Setting the version of the client app

The SDK provides a method +[AACSession setClientAppVersion:] for setting the current version of your app. An app version is used with analytics to make it easy to track issues between app versions and general analytics around versions.

Version strings longer than 128 characters will be trimmed to that length.

The client app version defaults to unknown if you do not call this method.

The following code snippet sets the client app's version to Version 14.2 (14C18).

AACSession.setClientAppVersion("Version 14.2 (14C18)")

Third-party dependencies

Atomic iOS SDK contains three third-party dependencies:

  • Font Awesome Free 5.15.4, an icon font that is used for card icons. This is distributed as an OTF font.
  • TrustKit 1.6.5, used to implement public key certificate pinning.
  • SocketRocket 0.6.0, used to support Websocket.
  • SwiftDraw 0.18.0, used to parse SVG images.

Atomic iOS SDK does not use any dependency managers, all dependencies are integrated manually.