iOS SDK - Current (24.2.4)
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.2.4.
The latest beta release is 24.3.0-beta1.
This documentation only applies to the latest stable version or earlier. For more recent beta versions of the iOS SDK, see iOS SDK beta guide.
Supported iOS version
The SDK supports iOS 10.0 and above.
As of version 23.3.0, the SDK supports iOS 12.0 and above. Support for iOS 10 and 11 has been discontinued.
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
- 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'
- Add the SDK as a dependency. You have two options available:
AtomicSDK
: the Atomic SDK distributed as anxcframework
, 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.2.4'
# or
pod 'AtomicSDK-framework', '24.2.4'
- 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
- Add
github "atomic-app/action-cards-ios-sdk-releases"
to yourCartfile
. - Run
carthage update
. - Follow the instructions provided by Carthage to add the SDK to your app.
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
- Open your Xcode project, and choose File > Add Packages.
- Enter
https://github.com/atomic-app/action-cards-ios-sdk-releases
in the upper right text field 'Search or Enter Package URL'. - Set the dependency rule and click 'Add Package'.
Manual Installation
- You can download releases of the SDK from the Releases page on Github.
- Once you've downloaded the version you need, navigate to your project in Xcode and select the "General" settings tab.
- Drag either
AtomicSDK.xcframework
orAtomicSDK.framework
from the directory where you unzipped the release, to theEmbedded Binaries
section. - When prompted, ensure that "Copy items if needed" is selected, and then click "Finish".
- If you chose AtomicSDK.framework above, you will also need to run the
strip-frameworks.sh
script (downloadable from this repository) as part of aRun Script
phase in your target, to get around an App Store submission bug, caused by iOS simulator architectures being present in the fat framework.
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:
- In the Workbench, click on the cog icon in the bottom left and select 'Settings';
- On the screen that appears, click the 'SDK' tab. Your API base URL is displayed in the 'API Host' section.
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:
-
By adding the following to your app's
Info.plist
file, replacingAPI_BASE_URL
with your URL: -
By declaring your API base URL in code, replacing
API_BASE_URL
with your URL:
- Swift
- Objective-C
if let url = URL(string: "API_BASE_URL") {
AACSession.setApiBaseUrl(url)
}
NSURL *url = [NSURL URLWithString:@"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.
- Swift
- Objective-C
AACSession.initialise(withEnvironmentId: "<environmentId>", apiKey: "<apiKey>")
[AACSession initialiseWithEnvironmentId:@"<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.
- Swift
- Objective-C
AACSession.setSessionDelegate(<the session delegate>)
[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.
- Swift
- Objective-C
func expiryInterval() -> TimeInterval {
return 60
}
- (NSTimeInterval)expiryInterval {
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.
- Swift
- Objective-C
func retryInterval() -> TimeInterval {
return 60
}
- (NSTimeInterval)retryInterval {
return 60;
}
Convenient initialization method
As of version 1.3.0, 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.
- Swift
- Objective-C
AACSession.login(withEnvironmentId: "<environmentId>", apiKey: "<apiKey>", sessionDelegate: <the session delegate>, apiBaseUrl: <the API base URL>)
[AACSession loginWithEnvironmentId:@"<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.
- Swift
- Objective-C
AACSession.setApiProtocol(.http)
[AACSession setApiProtocol:AACApiProtocolHttp];
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.
As of Release 1.3.0, the following behaviors have been applied 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 toYES
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.
- Swift
- Objective-C
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
}
}
}
}
[AACSession logout:^(NSError *error) {
if(error != nil) {
AACSessionLogoutErrorCode errorCode = error.code;
switch (errorCode) {
case AACSessionLogoutErrorCodeDataError: {
// Deal with data error.
NSError *dataError = error.userInfo[NSUnderlyingErrorKey];
}
break;
case AACSessionLogoutErrorCodeNetworkError: {
// Deal with network error.
NSError *networkError = error.userInfo[NSUnderlyingErrorKey];
}
break;
}
}
}];
The following code snippet shows how to de-register push notifications when logging out.
- Swift
- Objective-C
AACSession.logout(withNotificationsDeregistered: true) { error in
// The completion handler.
}
[AACSession logoutWithNotificationsDeregistered:YES completionHandler:^(NSError *error) {
// 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:
- A stream container ID, which uniquely identifies the stream container in the app;
- 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, orBack
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 toAACConfigurationInterfaceStyleAutomatic
. 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. As of version 23.3.0, 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 theAACCustomStringCardListFooterMessage
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 []
.
- Swift
- Objective-C
let config = AACConfiguration()
config.enabledUiElements = []
AACConfiguration *configuration = [[AACConfiguration alloc] init];
config.enabledUiElements = AACUIElementNone;
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 than200
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 thecardWidth
property, and it must be > 0.
The following code snippet sets the maximum card width to 500.
- Swift
- Objective-C
let config = AACConfiguration()
config.cardMaxWidth = 500
let streamContainer = AACStreamContainerViewController(identifier: "1234", configuration: config)
present(streamContainer, animated: true)
AACConfiguration *config = [[AACConfiguration alloc] init];
config.cardMaxWidth = 500;
AACStreamContainerViewController *streamContainer = [[AACStreamContainerViewController alloc] initWithIdentifier:@"1234" configuration:config];
[self presentViewController:streamContainer animated:YES completion:nil];
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.
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 yourruntimeVariableDelegate
'scardSessionDidRequestRuntimeVariables:completionHandler:
method. If you do not call the providedcompletionHandler
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 requiresenabledUiElements
to containAACUIElementCardListFooterMessage
. 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".
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:
- Swift
- Objective-C
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)
AACConfiguration *config = [[AACConfiguration alloc] init];
config.presentationStyle = AACConfigurationPresentationStyleWithContextualButton;
config.launchBackgroundColor = [UIColor whiteColor];
config.launchIconColor = [UIColor blueColor];
config.launchButtonColor = [UIColor blueColor];
config.launchTextColor = [UIColor whiteColor];
AACStreamContainerViewController *streamContainer = [[AACStreamContainerViewController alloc] initWithIdentifier:@"1234" configuration:config];
[self presentViewController:streamContainer animated:YES completion:nil];
Displaying a custom header
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.
- Swift
- Objective-C
// 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
// 1. Define the delegate.
@implementation ACSCustomHeaderDelegate
// The singleton is only for demonstration. It's not compulsory.
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static ACSCustomHeaderDelegate *headerDelegate;
dispatch_once(&onceToken, ^{
headerDelegate = [[ACSCustomHeaderDelegate alloc] init];
});
return headerDelegate;
}
- (void)tapped {
NSLog(@"You tapped the button.");
}
- (UIView *)streamContainerDidRequestCustomHeader:(AACStreamContainerViewController *)streamContainer proposedWidth:(CGFloat)proposedWidth {
self.streamContainer = streamContainer;
UIView *topView = [[UIView alloc] initWithFrame:CGRectMake(0, 10, proposedWidth, 90)];
topView.backgroundColor = [UIColor blueColor];
UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(30, 20, 0, 0)];
title.text = @"Name: John Doe";
title.textColor = [UIColor whiteColor];
[title sizeToFit];
[topView addSubview:title];
UILabel *title2 = [[UILabel alloc] initWithFrame:CGRectMake(30, 45, 0, 0)];
title2.text = [NSString stringWithFormat:@"Container ID: %@", streamContainer.identifier];
title2.textColor = [UIColor whiteColor];
[title2 sizeToFit];
[topView addSubview:title2];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 110, proposedWidth-20, 40)];
button.backgroundColor = [UIColor redColor];
[button setTitle:@"Tap this" forState:UIControlStateNormal];
[button addTarget:self action:@selector(tapped) forControlEvents:UIControlEventTouchUpInside];
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, proposedWidth, 160)];
header.backgroundColor = [UIColor clearColor];
[header addSubview:topView];
[header addSubview:button];
return header;
}
@end
// 2. Assign the delegate to the configuration
AACConfiguration *configuration = [[AACConfiguration alloc] init];
configuration.customHeaderDelegate = ACSCustomHeaderDelegate.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:
- The ID of the stream container to render in the horizontal container view.
- 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.
- Swift
- Objective-C
let config = AACHorizontalContainerConfiguration()
config.cardWidth = 400
let horizontalView = AACHorizontalContainerView(frame: self.view.bounds, containerIdentifier: "1234", configuration: config)
self.view.addSubview(horizontalView)
AACHorizontalContainerConfiguration *config = [[AACHorizontalContainerConfiguration alloc] init];
config.cardWidth = 400;
AACHorizontalContainerView *horizontalContainerView = [[AACHorizontalContainerView alloc] initWithFrame:self.view.bounds containerIdentifier:@"1234" configuration:config];
[self.view addSubview:horizontalContainerView];
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.
- Swift
- Objective-C
func horizontalContainerView(_ containerView: AACHorizontalContainerView, willChange newSize: CGSize) {
// Perform animation here.
}
- (void)horizontalContainerView:(AACHorizontalContainerView *)containerView willChangeSize:(CGSize)newSize {
// 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.
AACUIElementCardListFooterMessage
andAACCustomStringCardListFooterMessage
have no effects in horizontal container view.presentationStyle
is alwaysAACConfigurationPresentationStyleWithoutButton
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:
- 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;
- 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.
- Swift
- Objective-C
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)
AACConfiguration *config = [[AACConfiguration alloc] init];
config.launchBackgroundColor = [UIColor whiteColor];
config.launchIconColor = [UIColor blueColor];
config.launchButtonColor = [UIColor blueColor];
config.launchTextColor = [UIColor whiteColor];
AACSingleCardView *singleCardView = [[AACSingleCardView alloc] initWithFrame:self.view.frame containerIdentifier:@"1234" configuration:config];
[self.view addSubview:singleCardView];
As of version 23.3.0, toast messages - such as those seen when submitting, dismissing or snoozing a card in a stream container - are available within single card view. 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.
- Swift
- Objective-C
func singleCardView(_ cardView: AACSingleCardView, willChange newSize: CGSize) {
// Perform animation here.
}
- (void)singleCardView:(AACSingleCardView *)cardView willChangeSize:(CGSize)newSize {
// 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.
- Swift
- Objective-C
let config = AACSingleCardConfiguration()
config.automaticallyLoadNextCard = true
AACSingleCardConfiguration *config = [[AACSingleCardConfiguration alloc] init];
config.automaticallyLoadNextCard = YES;
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 toNO
.
Toasts for single card view
(introduced in 23.3.0)
You can now 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.
- Swift
- Objective-C
let config = AACSingleCardConfiguration()
config.enabledUiElements = .cardListToast
let singleCardView = AACSingleCardView(frame: self.view.frame, containerIdentifier: "1234", configuration: config)
self.view.addSubview(singleCardView)
AACSingleCardConfiguration *configuration = [[AACSingleCardConfiguration alloc] init];
config.enabledUiElements = AACUIElementCardListToast;
AACSingleCardView *singleCardView = [[AACSingleCardView alloc] initWithFrame:self.view.frame containerIdentifier:@"1234" configuration:config];
[self.view addSubview:singleCardView];
To disable the toast, simply set enabledUiElements
to AACUIElementNone
, or []
in Swift.
- Swift
- Objective-C
let config = AACSingleCardConfiguration()
config.enabledUiElements = []
AACSingleCardConfiguration *configuration = [[AACSingleCardConfiguration alloc] init];
config.enabledUiElements = AACUIElementNone;
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:
- Swift
- Objective-C
// 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.")
}
}
// Observe the stream container
[AACSession observeStreamContainerWithIdentifier:@"1" configuration:nil completionHandler:^(NSArray<AACCardInstance *> *cards) {
if(cards != nil) {
NSLog(@"There are %ld cards in the container", cards.count);
}
}];
This method returns a token that you can use to stop the observation, see Stopping the observation for more details.
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:
- Swift
- Objective-C
let token = AACSession.observeStreamContainer(identifier: "1", configuration: nil) { _ in }
AACSession.stopObservingStreamContainer(token: token)
id token = [AACSession observeStreamContainerWithIdentifier:@"1" configuration:nil completionHandler:^(NSArray<AACCardInstance *> *cards) {}];
[AACSession stopObservingStreamContainer: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.
- Swift
- Objective-C
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").")
}
}
[AACSession observeStreamContainerWithIdentifier:@"1" configuration:nil completionHandler:^(NSArray<AACCardInstance *> *cards) {
AACCardInstance *card = [cards firstObject];
if(card != nil) {
NSLog(@"The card instance ID is %@.", card.detail.cardId);
NSLog(@"The priority of the card is %@.", card.metadata.priority);
NSLog(@"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.
- Swift
- Objective-C
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).")
}
}
}
}
[AACSession observeStreamContainerWithIdentifier:@"1" configuration:nil completionHandler:^(NSArray<AACCardInstance *> *cards) {
AACCardInstance *card = [cards firstObject];
if(card != nil) {
for (AACCardNode *node in card.defaultLayout.nodes) {
if([node isKindOfClass:AACCardNodeCategory.class]) {
AACCardNodeCategory *category = (AACCardNodeCategory *)node;
NSLog(@"The category of card %@ is %@.", card.detail.cardId, 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.
- Swift
- Objective-C
AACSession.observeStreamContainer(identifier: "1", configuration: nil) { cards in
if let card = cards?.first, let subview = card.layout(withName: "subviewID") {
print("Accessing subview \(subview.title).")
}
}
[AACSession observeStreamContainerWithIdentifier:@"1" configuration:nil completionHandler:^(NSArray<AACCardInstance *> *cards) {
AACCardInstance *card = [cards firstObject];
AACCardLayout *subview = [card layoutWithName:@"subviewID"];
if(subview != nil) {
NSLog(@"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:
-
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. -
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.
- Swift
- Objective-C
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.")
}
}
AACSessionCardAction *action = [[AACSessionCardAction alloc] initDismissActionWithContainerId:@"1" cardId:@"card-id"];
[AACSession onCardAction:action completionHandler:^(NSError *error) {
if(error != nil) {
NSLog(@"An error happened when dismissing the card.");
} else {
NSLog(@"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.
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.
- Swift
- Objective-C
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)")
}
}
}
}
[AACSession observeStreamContainerWithIdentifier:@"1" configuration:nil completionHandler:^(NSArray<AACCardInstance *> *cards) {
AACCardInstance *card = [cards firstObject];
if(card != nil) {
// Pass `nil` for top-level buttons, or a subview ID for subview buttons.
NSArray *buttons = [card buttonsWithSubviewId:nil];
for(AACCardBaseButton *button in buttons) {
if([button isKindOfClass:AACCardNodeSubmitButton.class]) {
AACCardNodeSubmitButton *submitButton = (AACCardNodeSubmitButton *)button;
NSLog(@"The button name of submit button %@ is %@", submitButton.text, submitButton.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.
- Swift
- Objective-C
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.")
}
}
AACSessionCardAction *action = [[AACSessionCardAction alloc] initSubmitActionWithContainerId:@"1"
cardId:@"card-id"
submitButtonName:@"button-name"
submitValues:@{
@"submit-key": @"submitted-values",
@"submit-key2": @(999)
}];
[AACSession onCardAction:action completionHandler:^(NSError *error) {
if(error != nil) {
NSLog(@"An error happened when submitting the card.");
} else {
NSLog(@"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.
- Swift
- Objective-C
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.")
}
}
AACSessionCardAction *action = [[AACSessionCardAction alloc] initSubmitActionWithContainerId:@"1" cardId:@"card-id" submitValues:@{
@"submit-key": @"submitted-values",
@"submit-key2": @(999)
}];
[AACSession onCardAction:action completionHandler:^(NSError *error) {
if(error != nil) {
NSLog(@"An error happened when submitting the card.");
} else {
NSLog(@"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.
- Swift
- Objective-C
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.")
}
}
AACSessionCardAction *action = [[AACSessionCardAction alloc] initSnoozeActionWithContainerId:@"1" cardId:@"card-id" snoozeInterval: 60];
[AACSession onCardAction:action completionHandler:^(NSError *error) {
if(error != nil) {
NSLog(@"An error happened when snoozing the card.");
} else {
NSLog(@"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 toAACConfigurationInterfaceStyleLight
.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 attribute | Description | Value type |
---|---|---|
Priority | Card priority defined in Workbench, Card -> Delivery | NSInteger |
Card template created date | The date time when a card template is created | NSDate |
Card template ID | The template ID of a card, see below for how to get it | NSString |
Card template name | The template name of a card | NSString |
Custom variable | The variables defined for a card in Workbench, Card -> Variables | Multiple |
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.
- Swift
- Objective-C
let filterValue = AACCardFilterValue.byPriority(6)
AACCardFilterValue *filterValue = [AACCardFilterValue byPriority:6];
Custom variable
The following code snippet shows how to create a filter value that represents a boolean custom variable isSpecial
.
- Swift
- Objective-C
let filterValue = AACCardFilterValue.byVariableName("isSpecial", boolean: true)
AACCardFilterValue *filterValue = [AACCardFilterValue byVariableName:@"isSpecial" boolean:YES];
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:]
On the card editing page, click on the ID part of the overflow menu at the upper-right corner.
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.
Operator | Description | Supported types |
---|---|---|
equalTo | Equal to the filter value | NSInteger, NSDate, NSString, BOOL |
notEqualTo | Not equal to the filter value | NSInteger, NSDate, NSString, BOOL |
greaterThan | Greater than the filter value | NSInteger, NSDate |
greaterThanOrEqualTo | Greater than or equal to the filter value | NSInteger, NSDate |
lessThan | Less than the filter value | NSInteger, NSDate |
lessThanOrEqualTo | Less than or equal to the filter value | NSInteger, NSDate |
in | In one of the filter values | NSInteger, NSDate, NSString |
notIn | Not in one of the filter values | NSInteger, NSDate, NSString |
between | In the range of start and end, inclusive | NSInteger, 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.
- Swift
- Objective-C
let filterValue1 = AACCardFilterValue.byPriority(2)
let filterValue2 = AACCardFilterValue.byPriority(6)
let filter = AACCardListFilter.filter(byCardsBetweenStartValue: filterValue1, endValue: filterValue2)
AACCardFilterValue *filterValue1 = [AACCardFilterValue byPriority:2];
AACCardFilterValue *filterValue2 = [AACCardFilterValue byPriority:6];
AACCardFilter *filter = [AACCardListFilter filterByCardsBetweenStartValue:filterValue1 endValue:filterValue2];
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:
-
Create one or more
AACCardFilterValue
objects. -
Combine filter values with filter operators to form a
AACCardFilter
. -
Apply filter(s).
3.1. For stream containers, call
to apply multiple filters, otherwise call-[AACStreamContainer applyFilters:]
. To delete all existing filters, pass either-[AACStreamContainer applyFilter:]
nil
or an empty list[]
to theapplyFilters
method, ornil
to theapplyFilter
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.
- Swift
- Objective-C
let filterValue = AACCardFilterValue.byPriority(5)
let filter = AACCardListFilter.filter(byCardsGreaterThan: filterValue)
...
// Acquire the stream container object and call the filtering method.
streamContainer.apply(filter)
AACCardFilterValue *filterValue = [AACCardFilterValue byPriority:5];
AACCardFilter *filter = [AACCardListFilter filterByCardsGreaterThan:filterValue];
...
// Acquire the stream container object and call the filtering method.
[streamContainer applyFilter: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.
- Swift
- Objective-C
let filterValue = AACCardFilterValue.byCreatedDate(Date(timeIntervalSince1970: 1673226000))
let filter = AACCardListFilter.filter(byCardsLessThanOrEqualTo: filterValue)
...
streamContainer.apply(filter)
AACCardFilterValue *filterValue = [AACCardFilterValue byCreatedDate:[NSDate dateWithTimeIntervalSince1970:1673226000]];
AACCardFilter *filter = [AACCardListFilter filterByCardsLessThanOrEqualTo:filterValue];
...
[streamContainer applyFilter: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.
- Swift
- Objective-C
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)
AACCardFilterValue *filterValue1 = [AACCardFilterValue byCardTemplateName:@"card1"];
AACCardFilterValue *filterValue2 = [AACCardFilterValue byCardTemplateName:@"card2"];
AACCardFilterValue *filterValue3 = [AACCardFilterValue byCardTemplateName:@"card3"];
AACCardFilter *filter = [AACCardListFilter filterByCardsIn:@[filterValue1, filterValue2, filterValue3]];
...
[streamContainer applyFilter: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.
- Swift
- Objective-C
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])
AACCardFilterValue *filterValue1 = [AACCardFilterValue byPriority:6];
AACCardFilter *filter1 = [AACCardListFilter filterByCardsNotEqualTo:filterValue1];
AACCardFilterValue *filterValue2 = [AACCardFilterValue byVariableName:@"isSpecial" boolean:YES];
AACCardFilter *filter2 = [AACCardListFilter filterByCardsEqualTo:filterValue2];
...
[streamContainer applyFilters:@[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.
- Swift
- Objective-C
let filter = AACCardListFilter.filter(byCardInstanceId: "ABCD-1234")
streamContainer.apply(filter)
AACCardFilter *filter = [AACCardListFilter filterByCardInstanceId:@"ABCD-1234"];
[streamContainer applyFilter:filter];
Removing all filters
- For stream containers, specify
nil
or an empty list[]
to the method, or-[AACStreamContainer applyFilters:]
nil
to the method.-[AACStreamContainer applyFilter:]
- For card count observers, create a new card counter observer with
nil
to thefilters
parameter.
Supporting custom actions on submit and link buttons
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.
- Swift
- Objective-C
// 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
}
}
// 1. Assign the action delegate
AACConfiguration *config = [[AACConfiguration alloc] init];
config.actionDelegate = self;
// 2. Implement the callbacks
- (void)streamContainerDidTapLinkButton:(AACStreamContainerViewController*)streamContainer withAction:(AACCardCustomAction*)action {
if([action.actionPayload[@"screen"] isEqualToString:@"home-screen"]) {
[self navigateToHomeScreen];
}
}
- (void)streamContainerDidTapSubmitButton:(AACStreamContainerViewController*)streamContainer withAction:(AACCardCustomAction*)action {
if([action.actionPayload[@"outcome"] isEqualToString:@"success"]) {
[self navigateToSuccessScreen];
}
}
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".
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:
- Swift
- Objective-C
config.setValue("Snooze", for: .cardSnoozeTitle)
[configuration setValue:@"Snooze" forCustomString:AACCustomStringCardSnoozeTitle];
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:
- Swift
- Objective-C
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)
AACConfiguration *config = [[AACConfiguration alloc] init];
config.cardVotingOptions = AACCardVotingOptionUseful | AACCardVotingOptionNotUseful;
config.cardVotingOptions = AACCardVotingOptionUseful;
config.cardVotingOptions = AACCardVotingOptionNone;
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:
- Swift
- Objective-C
let config = AACConfiguration()
config.setValue("Provide feedback", for: .votingFeedbackTitle)
config.setValue("Thumbs up", for: .votingUseful)
config.setValue("Thumbs down", for: .votingNotUseful)
AACConfiguration *config = [[AACConfiguration alloc] init];
[config setValue:@"Provide feedback" forCustomString:AACCustomStringVotingFeedbackTitle];
[config setValue:@"Thumbs up" forCustomString:AACCustomStringVotingUseful];
[config setValue:@"Thumbs down" forCustomString:AACCustomStringVotingNotUseful];
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.
- Swift
- Objective-C
streamContainer.refresh()
[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).
To be notified when these happen, assign a card event delegate to your stream container:
- Swift
- Objective-C
// 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.
}
// 1. Assign the event delegate
AACConfiguration *config = [[AACConfiguration alloc] init];
config.cardEventDelegate = self;
// 2. Implement the delegate
- (void)streamContainer:(AACStreamContainerViewController *)streamContainerVc didTriggerCardEvent:(AACCardEvent *)event {
// 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.
- Swift
- Objective-C
// 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.
}
AACCustomEvent *customEvent = [[AACCustomEvent alloc] initWithName:@"myEvent" properties:@{
@"firstName": @"John",
@"secondName": @"Smith"
}];
[AACSession sendCustomEvent:customEvent completionHandler:^(NSError *error) {
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:
- Swift
- Objective-C
AACSession.registerStreamContainers(forPushNotifications: ["1"])
[AACSession registerStreamContainersForPushNotifications:@[@"1"] completionHandler:nil];
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.
- Swift
- Objective-C
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
}
}
}
}
[AACSession registerStreamContainersForPushNotifications:@[ containerId ]
completionHandler:^(NSError * _Nullable error) {
if(error != nil) {
AACSessionPushRegistrationErrorCode errorCode = error.code;
switch (errorCode) {
case AACSessionPushRegistrationErrorDomainDataError: {
// Deal with data error.
NSError *dataError = error.userInfo[NSUnderlyingErrorKey];
}
break;
case AACSessionPushRegistrationErrorCodeNetworkError: {
// Deal with network error.
NSError *networkError = error.userInfo[NSUnderlyingErrorKey];
}
break;
}
}
}];
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.
- Swift
- Objective-C
AACSession.registerStreamContainers(forPushNotifications: ["1"], notificationsEnabled: false)
[AACSession registerStreamContainersForPushNotifications:@[ containerId ]
notificationsEnabled:NO
completionHandler:^(NSError * error) {
// Do something.
}];
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.
- Swift
- Objective-C
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
}
}
}
}
[AACSession deregisterDeviceForNotificationsWithCompletionHandler:^(NSError * _Nullable error) {
if(error != nil) {
AACSessionPushRegistrationErrorCode errorCode = error.code;
switch (errorCode) {
case AACSessionPushRegistrationErrorDomainDataError: {
// Deal with data error.
NSError *dataError = error.userInfo[NSUnderlyingErrorKey];
}
break;
case AACSessionPushRegistrationErrorCodeNetworkError: {
// Deal with network error.
NSError *networkError = error.userInfo[NSUnderlyingErrorKey];
}
break;
}
}
}];
2. Send the push token to the Atomic Platform
Send the device's push token to the Atomic platform when it changes, by calling:
- Swift
- Objective-C
AACSession.registerDevice(forNotifications:completionHandler:)
+[AACSession registerDeviceForNotifications: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.
- Swift
- Objective-C
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
}
}
}
}
[AACSession registerDeviceForNotifications:token completionHandler:^(NSError *error) {
if(error != nil) {
AACSessionPushRegistrationErrorCode errorCode = error.code;
switch (errorCode) {
case AACSessionPushRegistrationErrorDomainDataError: {
// Deal with data error.
NSError *dataError = error.userInfo[NSUnderlyingErrorKey];
}
break;
case AACSessionPushRegistrationErrorCodeNetworkError: {
// Deal with network error.
NSError *networkError = error.userInfo[NSUnderlyingErrorKey];
}
break;
}
}
}];
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.
- Swift
- Objective-C
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()
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
if([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) {
AACPushNotification *notification = [AACSession notificationFromPushPayload:response.notification.request.content.userInfo];
if(notification != nil) {
// 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.
- Swift
- Objective-C
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)
}
}
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *))contentHandler {
// It is recommended to use the login method to initialize the SDK.
[AACSession loginWithEnvironmentId:@"<environmentId>" apiKey:@"<apiKey>" sessionDelegate:<Your session delegate> apiBaseUrl:<API base URL>];
[AACSession trackPushNotificationReceived:request.content.userInfo completionHandler:^(NSError * _Nullable error) {
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
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.
- Swift
- Objective-C
// 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.")
}
}
// Observe the card count
[AACSession observeCardCountForStreamContainerWithIdentifier:@"1" interval:15 handler:^(NSNumber *cardCount) {
NSLog(@"There are %@ cards in the container", cardCount);
}];
// Retrieve a one-off card count
[AACSession requestCardCountForStreamContainerWithIdentifier:@"1" handler:^(NSNumber *cardCount) {
NSLog(@"There are %@ cards in the container", cardCount);
}];
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:
- Swift
- Objective-C
AACSession.stopObservingCardCount(token)
[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:
- Swift
- Objective-C
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).")
}
}
[[NSNotificationCenter defaultCenter] addObserverForName:AACSessionCardCountDidChange
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSNumber *visibleCards = note.userInfo[AACSessionCardCountUserInfoKey];
NSLog(@"*** There are %@ visible cards in stream container %@.", visibleCards, note.object);
}];
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
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.
- Swift
- Objective-C
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"))")
}
}
[AACSession userMetricsWithCompletionHandler:^(AACUserMetrics *response, NSError *error) {
if(response != nil) {
NSLog(@"Total cards: %i", [response totalCards]);
NSLog(@"Total cards in stream container: %i", [response totalCardsForStreamContainerWithId:@"containerId"]);
NSLog(@"Unseen cards: %i", [response unseenCards]);
NSLog(@"Unseen cards in stream container: %i", [response unseenCardsForStreamContainerWithId:@"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.
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.
- Swift
- Objective-C
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)
}
- (void)cardSessionDidRequestRuntimeVariables:(NSArray<AACCardInstance*>*)cardsToResolve completionHandler:(AACSessionRuntimeVariablesHandler)completionHandler {
for(AACCardInstance* instance in cardsToResolve) {
// Resolve variables on all cards.
// You can also inspect `lifecycleId` and `cardInstanceId` to determine what type of card this is.
[instance resolveRuntimeVariableWithName:@"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
:
- Swift
- Objective-C
streamVc.updateVariables()
[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:
- Open the Atomic Workbench;
- Navigate to Configuration > SDK > Container themes. Select the theme you want to edit.
- When editing a typography style in the theme (e.g. Headline > Typography), turn
Dynamic scaling
on. - 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.
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 size | Delta applied |
---|---|
XS | -3 |
S | -2 |
M | -1 |
L (default) | 0 |
XL | 2 |
XXL | 4 |
XXXL | 6 |
Accessibility M | 10 |
Accessibility L | 15 |
Accessibility XL | 20 |
Accessibility XXL | 25 |
Accessibility XXXL | 30 |
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 size | Resultant size |
---|---|
XS | 15px (14px is below min) |
S | 15px (at or above min) |
M | 16px |
L (default) | 17px |
XL | 19px |
XXL | 21px |
XXXL | 23px |
Accessibility M | 27px |
Accessibility L | 30px (32px is above max) |
Accessibility XL | 30px (37px is above max) |
Accessibility XXL | 30px (42px is above max) |
Accessibility XXXL | 30px (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 ofAACFontWeight
, 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:
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.
- Swift
- Objective-C
AACSession.register([
AACEmbeddedFont(familyName: "BrandFont", postscriptName: "HelveticaNeue", weight: .weightBold, style: .italic)
])
[AACSession registerEmbeddedFonts:@[
[[AACEmbeddedFont alloc] initWithFamilyName:@"BrandFont"
postscriptName:@"HelveticaNeue"
weight:AACFontWeightBold
style:AACFontStyleItalic];
]];
SDK Analytics
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:
- Swift
- Objective-C
let config = AACConfiguration()
config.features.runtimeVariableAnalytics = true
AACConfiguration *config = [[AACConfiguration alloc] init];
config.features.runtimeVariableAnalytics = YES;
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.
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.
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.
- Swift
- Objective-C
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)
- (AACRequestDisposition *)dispositionForAtomicRequest:(NSURL *)requestUrl {
if([[requestUrl host] isEqualToString:@"atomic.io"]) {
// Allow requests to atomic.io with certificate pinning.
NSSet *pins = [NSSet setWithArray:@[
[AACCertificatePin pinWithSha256Hash:@"AAAAAAAA="]
]];
return [AACRequestDisposition allowWithCertificatePins:pins];
}
if([[requestUrl host] isEqualToString:@"placeholder.com"]) {
// Always allow requests to placeholder.com.
return [AACRequestDisposition allow];
}
// Deny all other requests.
return [AACRequestDisposition 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:]
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.
- Swift
- Objective-C
let settings = AACUserSettings()
// Set up some basic profile fields.
settings.externalID = "an external ID"
settings.name = "John Smith"
AACSession.updateUser(settings)
AACUserSettings *settings = [[AACUserSettings alloc] init];
// Set up some basic profile fields.
settings.externalID = @"an external ID";
settings.name = @"John Smith";
[AACSession updateUser:settings completionHandler:nil];
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:]
-[AACUserSettings setText:forCustomField:]
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.
- Swift
- Objective-C
settings.setDate(Date(timeIntervalSinceNow: 0), forCustomField: "custom_date_field")
settings.setText("some custom texts", forCustomField: "custom_text_field")
[settings setDate:[NSDate dateWithTimeIntervalSinceNow: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 isYES
.setNotificationTime:weekday
: An optional method that defines the notification time preferences of the user for different days of the week. If you specifyAACUserNotificationTimeframeWeekdaysDefault
to theweekday
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.
- Swift
- Objective-C
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)
AACUserSettings *settings = [[AACUserSettings alloc] init];
NSArray *timeFrames = @[[[AACUserNotificationTimeframe alloc] initWithStartHour:8
startMinute:0
endHour:17
endMinute:30],
[[AACUserNotificationTimeframe alloc] initWithStartHour:19
startMinute:0
endHour:22
endMinute:00]];
[settings setNotificationTime:timeframes weekday:AACUserNotificationTimeframeWeekdaysMonday];
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.
- Swift
- Objective-C
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
}
}
}
}
AACUserSettings *settings = [[AACUserSettings alloc] init];
// Set up some basic properties.
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:[NSDate dateWithTimeIntervalSinceNow:0] forCustomField:@"myCustomDateField"];
[settings setText:@"My custom value" forCustomField:@"myCustomTextField"];
settings.notificationsEnabled = YES;
// Set up notification timeframes from 08:30 to 17:45, on all work days.
[settings setNotificationTime:@[[[AACUserNotificationTimeframe alloc] initWithStartHour:8
startMinute:0
endHour:17
endMinute:45]]
weekday:AACUserNotificationTimeframeWeekdaysDefault];
[settings setNotificationTime:@[] weekday:AACUserNotificationTimeframeWeekdaysSaturday];
[settings setNotificationTime:@[] weekday:AACUserNotificationTimeframeWeekdaysSunday];
[AACSession updateUser:settings completionHandler:^(NSError *error) {
if(error != nil) {
AACSessionUpdateUserErrorCode errorCode = error.code;
switch (errorCode) {
case AACSessionUpdateUserErrorCodeDataError: {
// Deal with data error.
NSError *dataError = error.userInfo[NSUnderlyingErrorKey];
}
break;
case AACSessionUpdateUserErrorCodeNetworkError: {
// Deal with network error.
NSError *networkError = error.userInfo[NSUnderlyingErrorKey];
}
break;
}
}
}];
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
(introduced in 23.3.0)
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.
- Swift
- Objective-C
AACSession.observeSDKEvents { event in
// Do something with the event.
}
[AACSession observeSDKEventsWithCompletionHandler:^(AACSDKEvent *sdkEvent) {
// Do something with the event.
}];
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 name | Event class | Analytics | Description |
---|---|---|---|
card-dismissed | AACSDKEventCardDismissed | YES | The user dismisses a card. |
card-snoozed | AACSDKEventCardSnoozed | YES | The user snoozes a card. |
card-completed | AACSDKEventCardCompleted | YES | The user submits a card. |
card-feed-updated | AACSDKEventCardFeedUpdated | NO | A card feed has been updated |
card-displayed | AACSDKEventCardDisplayed | YES | A card is displayed in a container. |
card-voted-up | AACSDKEventCardVotedUp | YES | The user taps on the “This is useful” option. |
card-voted-down | AACSDKEventCardVotedDown | YES | The user taps the “Submit” button on the card feedback screen. |
runtime-vars-updated | AACSDKEventRuntimeVarsUpdated | YES | One or more runtime variables are resolved. |
stream-displayed | AACSDKEventStreamDisplayed | YES | A stream container is first loaded or returned to. |
user-redirected | AACSDKEventUserRedirected | YES | The user is redirected by a URL or a custom payload. |
snooze-options-displayed | AACSDKEventSnoozeOptionsDisplayed | YES | The snooze date/time selection UI is displayed. |
snooze-options-canceled | AACSDKEventSnoozeOptionsCanceled | YES | The user taps the “Cancel” button in the snooze UI. |
card-subview-displayed | AACSDKEventCardSubviewDisplayed | YES | A subview of card is opened. |
card-subview-exited | AACSDKEventCardSubviewExited | YES | The user leaves the subview. |
video-played | AACSDKEventVideoPlayed | YES | The user hits the play button of a video. |
video-completed | AACSDKEventVideoCompleted | YES | A video finishes playing. |
sdk-initialized | AACSDKEventSDKInitialized | YES | An instance of the SDK is initialized, or the JWT is refreshed. |
request-failed | AACSDKEventRequestFailed | YES | Any 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-received | AACSDKEventNotificationReceived | YES | A push notification is received by the SDK. |
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.
- Swift
- Objective-C
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
}
}
[AACSession observeSDKEventsWithCompletionHandler:^(AACSDKEvent *sdkEvent) {
switch(sdkEvent.eventType) {
case AACSDKEventTypeCardSubviewDisplayed: {
AACSDKEventCardSubviewDisplayed *subviewDisplayed = (AACSDKEventCardSubviewDisplayed *)sdkEvent;
if(subviewDisplayed != nil) {
NSLog(@"The subview %@ of card %@ has been displayed.", subviewDisplayed.subviewId, subviewDisplayed.cardInstanceId);
}
}
break;
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.
- Swift
- Objective-C
AACSession.observeSDKEvents { event in
let rawData = event.getRawContents()
// Inspect the raw data for more details.
}
[AACSession observeSDKEventsWithCompletionHandler:^(AACSDKEvent *sdkEvent) {
NSDictionary *rawData = [sdkEvent getRawContents];
// Inspect the raw data for more details.
}];
Stopping the observation
To stop the observation, call either [AACSession stopObservingSDKEvents]
[AACSession observeSDKEventsWithCompletionHandler:nil]
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.
- Swift
- Objective-C
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
}
}
[AACSession observeSDKEventsWithCompletionHandler:^(AACSDKEvent *sdkEvent) {
switch (sdkEvent.eventType) {
case AACSDKEventTypeCardDisplayed:
case AACSDKEventTypeCardFeedUpdated: {
// Fetch the user metrics.
[AACSession userMetricsWithCompletionHandler:^(AACUserMetrics *response, NSError *error) {
if(response != nil) {
NSString *containerId = @"1234";
NSInteger unseenCardsForContainer = [response unseenCardsForStreamContainerWithId:containerId];
NSLog(@"The unseen cards for container %@ is %ld", containerId, unseenCardsForContainer);
}
}];
}
break;
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.
- Swift
- Objective-C
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
}
}
[AACSession observeSDKEventsWithCompletionHandler:^(AACSDKEvent *sdkEvent) {
switch (sdkEvent.eventType) {
case AACSDKEventTypeCardVotedDown: {
AACSDKEventCardVotedDown *votedDown = (AACSDKEventCardVotedDown *)sdkEvent;
NSLog(@"The user has voted down for the card %@", votedDown.cardInstanceId);
switch (votedDown.reason) {
case AACSDKEventVoteDownReasonNotRelevant:
NSLog(@"The reason is it's not relevant.");
break;
case AACSDKEventVoteDownReasonTooOften:
NSLog(@"The reason is it's displayed too often.");
break;
case AACSDKEventVoteDownReasonOther:
NSLog(@"The user provided some other reasons: %@.", votedDown.otherMessage);
break;
}
}
break;
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.
- Swift
- Objective-C
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
}
}
[AACSession observeSDKEventsWithCompletionHandler:^(AACSDKEvent *sdkEvent) {
switch (sdkEvent.eventType) {
case AACSDKEventTypeUserRedirected: {
AACSDKEventUserRedirected *userRedirectedEvent = (AACSDKEventUserRedirected *)sdkEvent;
switch (userRedirectedEvent.detail) {
case AACSDKEventRedirectDetailTypeImage:
NSLog(@"Event triggered by an image.");
break;
case AACSDKEventRedirectDetailTypeLinkButton:
NSLog(@"Event triggered by a link button.");
break;
case AACSDKEventRedirectDetailTypeSubmitButton:
NSLog(@"Event triggered by a submit button.");
break;
case AACSDKEventRedirectDetailTypeTextLink:
NSLog(@"Event triggered by a markdown link text.");
break;
}
}
break;
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.
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.
- Swift
- Objective-C
// 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()
}
}
// 1. Assign the action delegate
AACConfiguration *config = [[AACConfiguration alloc] init];
config.actionDelegate = self;
// 2. Implement the callbacks
- (void)streamContainerDidTapLinkButton:(AACStreamContainerViewController*)streamContainer withAction:(AACCardCustomAction*)action {
if([action.actionPayload[@"screen"] isEqualToString:@"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:- Use the icon theme color if this has been supplied.
- Use the color of the text associated with the icon.
- 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:
- If the provided SVG image is inaccessible due to a broken URL or network issues, such as those caused by certificate pinning.
- 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:
- The fallback FontAwesome icon is used if it is set in Atomic Workbench for this custom icon.
- Otherwise, a default placeholder image is displayed.
Multiple display heights
(Introduced in 24.2.0)
This is currently a beta feature in Workbench.
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".
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.
- Swift
- Objective-C
AACSession.enableDebugMode(level)
[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.