The iOS SwiftUI SDK is currently in beta.
iOS SwiftUI SDK (25.1.1)
Introduction
This guide only includes features introduced in the SwiftUI SDK. For a full SDK guide of the latest stable version, see iOS SDK guide.
The Atomic iOS SwiftUI SDK is a dynamic framework for integrating an Atomic stream container into your SwiftUI app, presenting cards from a stream to your end users.
The SwiftUI SDK is written in Swift and supports iOS 16.0 and above.
The latest stable release of the SwiftUI SDK is 25.1.1
Installation
The SDK can be installed using Swift Package Manager, CocoaPods, or manually.
Swift Package Manager
- Open your Xcode project, and choose File > Add Packages.
- Enter
https://github.com/atomic-app/action-cards-swiftui-sdk-releases
in the upper right text field 'Search or Enter Package URL'. - Set the dependency rule and click 'Add Package'.
- Add both
AtomicSDK
andAtomicSwiftUISDK
to your target.
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.
pod 'AtomicCards', '25.1.1'
- Run
pod update
.
Alternative way to install Atomic SwiftUI SDK
Alternatively, you can install Atomic SDK directly through a git path. This will install the latest Atomic SwiftUI SDK.
pod 'AtomicCards', :git => 'https://github.com/atomic-app/action-cards-swiftui-sdk-releases.git'
Note: Currently you may face a known issue of a "Sandbox: rsync" failing message after integration. You can update your Xcode project build option ENABLE_USER_SCRIPT_SANDBOXING
to 'No' to resolve this issue.
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 both
AtomicSDK.xcframework
andAtomicSwiftUISDK.xcframework
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".
Initializing the SDK
As per the standard iOS SDK you need to initialise the SDK before you can do anything else. With SwiftUI you can place it in the init
function of your App
subclass. For example:
import AtomicSDK
@main
struct SwiftUIBoilerplateApp: App {
init() {
AACSession.login(withEnvironmentId: "<your envid>", apiKey: "<your api key>", sessionDelegate: <your session delegate>, apiBaseUrl: <your api base URL>)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Displaying containers
Displaying a vertical container
To display a vertical Stream Container in your view, call StreamContainer
within your views body
by specifying the flag indicating its presence in a navigation stack, and providing the container Id. StreamContainer
also accepts a configuration
object that can be ignored by default.
For example:
var body: some View {
NavigationStack {
VStack {
NavigationLink {
ZStack {
StreamContainer(isInNavigationStack: true, containerId: "<stream container id>")
.navigationTitle("Atomic Stream")
}
} label: {
Text("Messages")
}
}
.padding()
.navigationTitle("Atomic Boilerplate")
.navigationBarTitleDisplayMode(.large)
}
}
Properly setting the isInNavigationStack
flag is crucial. In SwiftUI, nested navigation stacks can lead to unexpected behaviors. As such, the container will not attempt to generate its own navigation stack if indicated that it is already within an existing navigation stack.
The configuration
object is of type ContainerConfiguration
and inherits most of the properties from AACConfiguration. The options have been adapted to align with SwiftUI conventions.
For example to set your configuration object in your view model you could do something like:
var config = ContainerConfiguration()
init() {
config.setCustomValue("test", for: .cardListTitle)
}
This example sets the stream header title to "test".
To use the configuration you could pass it into the StreamContainer
instance:
StreamContainer(isInNavigationStack: true, containerId: "1234", configuration: viewModel.config)
Closing a vertical container
When displaying a vertical container via modal view modifiers, such as .sheet
or .fullScreenCover
, add a button inside the view to dismiss it.
The following codes snippet shows how to place a close button on a full screen cover along with the container.
.fullScreenCover(isPresented: $isShowingMessagesView) {
VStack(alignment: .leading, spacing: 0) {
Button {
isShowingMessagesView = false
} label: {
Text("Close")
.font(.custom("Figtree-Medium", size: 16))
.foregroundStyle(.black)
}
.padding(15)
StreamContainer(isInNavigationStack: false,
containerId: "streamContainerId")
}
}
Displaying a horizontal container
The Atomic iOS SDK provides support for rendering a horizontal stream container within your host application. The horizontal view displays cards arranged from left to right.
To use this feature, instantiate a HorizontalContainer
, which is a SwiftUI View
configured similarly to a standard stream container. Upon initialization, please supply the following parameters:
- Stream Container ID: The identifier for the stream you wish to render.
- Card Width: The width of each card displayed (must be greater than 0).
- Configuration (optional): Defines the desired behavior and appearance of the horizontal container.
The available configuration options align with those of a standard stream container. However, please note that certain properties are not applicable to horizontal containers. For detailed information, refer to the definition file of ContainerConfiguration
.
Please ensure the HorizontalContainer
is placed within an existing NavigationStack
. The HorizontalContainer
itself does not generate a navigation stack to avoid putting navigation stacks inside a scroll view, which is a common parent of views like horizontal containers.
Additionally, please refrain from embedding the HorizontalContainer
within lazy containers (LazyVStack
or LazyHStack
), as SwiftUI restricts navigation destinations within lazy-loading containers.
The following example illustrates how to place a horizontal container above a LazyVStack
, using a card width of 350 and the default configuration.
var body: some View {
NavigationStack {
ScrollView {
VStack {
HorizontalContainer(containerId: "containerID", cardWidth: 350)
LazyVStack {
ForEach(0..<1000) { _ in Text("Placeholder") }
}
}
}
}
}
Note: Pull to refresh functionality and swipe gesture is disabled in horizontal containers.
Configuration options for a horizontal container
In addition to common properties from ContainerConfiguration
, the following properties are specifically designed for horizontal containers:
-
horizontalTitleAlignment
: Sets the alignment of the title in the horizontal header. Possible values:center
: Default value. Title is centered within the header.leading
: Title is aligned to the leading edge of the header.
-
horizontalEmptyStyle
: Defines how the container appears when no cards are present. Possible values:standard
: Default value. Displays a UI indicating no cards are available.shrink
: The container shrinks when there are no cards.
-
horizontalScrollMode
: Controls scrolling behavior in the container. Possible values:snap
: Available on iOS 17.0 and later. Default when available. Enables snapping, positioning each card in the center of the viewport upon scroll termination.free
: Default value for iOS 16. The container scrolls freely without snapping.
-
horizontalLastCardAlignment
: Aligns the card when only one card is present in the container. Possible values:leading
: Default value. Aligns the card to the leading edge.center
: Aligns the card to the center of the container.scaleToFill
: (Introduced in 25.1.0) Scales the card's width to fill the container with default padding.
Displaying a single card container
The Atomic SwiftUI SDK also supports rendering a single card in your host app.
To create an instance of SingleCardContainer
, which is configured in the same way as a vertical stream container, you supply the following parameters on instantiation:
- The ID of the stream container to render in the single card container. The single card container 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 container.
The configuration options, supplied using the configuration object above, are the same as those for a stream container. But some properties are not applied to the single card container. For detailed information, refer to the definition file of ContainerConfiguration
.
Please ensure the SingleCardContainer
is placed within an existing NavigationStack
. The SingleCardContainer
itself does not generate a navigation stack to avoid putting navigation stacks inside a scroll view, which is a common parent of views like horizontal containers.
Additionally, please refrain from embedding the SingleCardContainer
within lazy containers (LazyVStack
or LazyHStack
), as SwiftUI restricts navigation destinations within lazy-loading containers.
The code snippet below shows how to display a single card container with some configuration.
var body: some View {
var config = ContainerConfiguration()
config.enabledUIElements = [.cardListToast]
config.cardListRefreshInterval = 10
return NavigationStack {
ScrollView {
VStack {
Text("Header view")
SingleCardContainer(containerId: "containerID", configuration: config)
Text("Footer view")
}
}
}
}
Displaying a modal container (preview)
The Atomic iOS SDK provides support for displaying a modal container within your host application. The modal is displayed as soon as there are cards present in the associated stream container, and displays the first card over a fullscreen overlay, preventing interactions with views behind it. Users may only exit the modal after removing all the presented cards.
To integrate this feature, attach the .modalContainer
modifier to your SwiftUI view, providing the following parameters upon initialization:
- Stream Container ID: Identifier for the stream container you wish to display.
- Configuration (optional): Defines the desired appearance and behavior of the modal container.
The available configuration options align closely with those of a standard stream container; however, please note certain properties are not applicable to modal presentations. For detailed information, refer to the definition file of ContainerConfiguration
.
Please note that attaching the .modalContainer
modifier affects all subviews. As a result, the modal will be triggered even if the user has navigated to deeper subviews within your view hierarchy.
Several theme properties influence the modal container's appearance, including vertical alignment and padding. For further details, please refer to Modal Container Theme. Should the content within a card exceed the available screen height, the card will become vertically scrollable, ignoring any vertical alignment specified in the theme. Scrollable content is always aligned to the top of the scroll view.
The following example illustrates attaching a modal container to a SwiftUI view using the default configuration:
var body: some View {
Text("placeholder")
.modalContainer(containerId: "containerID")
}
Note: Swipe gestures are disabled within modal containers.
Maximum card width
You can 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 ContainerConfiguration
to define the desired width, and apply this configuration when initializing the stream container or the single card view.
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:
-
Setting a value below
200
display points is not recommended, as it may trigger layout constraint warnings when content cannot fit. -
Negative values behave the same as
0
display points. -
The padding between the card and container will always be at least 10 display points. For example, on a device with a 440-point screen, any card width exceeding 430 points will be ignored.
The following code snippet sets the maximum card width to 500.
var body: some View {
var configuration = ContainerConfiguration()
configuration.cardMaxWidth = 500
return NavigationStack {
VStack {
NavigationLink {
StreamContainer(isInNavigationStack: true, containerId: "<stream container id>", configuration: configuration)
} label: {
Text("Messages")
}
}
}
}
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 ContainerConfiguration
object, call the setCustomValue(_:for:)
method to customize the title for the card snooze functionality:
config.setCustomValue(snoozeTitle, for: .snoozeTitle)
Prevent Snoozing Beyond Expiry Date
Selecting a date & time combination beyond a card's expiry date is prevented when snoozing a card using the SDK's built-in interface.
There are three ways to snooze a card in the Atomic SDK:
- Overflow Menu: Select the overflow menu item (default "Remind me"), then choose a date & time in the built-in selector.
- Snooze Button: Tap a snooze button on the card and select a date & time via the built-in selector.
- Pre-set Snooze Button: Tap a snooze button with a pre-set snooze period, which snoozes the card immediately without showing the selector.
If an expiry date is set on the card, you cannot select dates beyond this expiry in scenarios 1 and 2. Scenario 3 remains unaffected, as the snooze period is explicitly pre-configured in Workbench.
File Upload
This is currently a beta feature in Workbench.
The File Upload element allows you to add a component to a card through which users can upload a file. Currently, only images can be selected. Multiple updates in the SDK support its customization.
Currently, only static images can be uploaded; GIFs and non-image file types are not supported. Images in formats other than JPEG and PNG are converted to JPEG before uploading. For Live Photos, only their key photos will be uploaded.
Preparing to Use File Upload
The File Upload element enables users to upload a picture taken from the camera or selected from the photo library. However, you must set up an NSCameraUsageDescription
key in the host app's Info.plist
, with a string value explaining to the user how the app uses this data. The app will crash if that key isn't set and the user taps "Take Photo" in the photo pick menu of File Upload element. See Apple's documentation for more details.
Configuring Camera Access Request Toast
If the user initially denies the camera access request, the iOS's built-in request dialog won't appear again due to iOS behaviour. By default, when the user tries to take a photo under these circumstances, Atomic SDK will show a toast message at the bottom, prompting them to enable camera access. The user can tap a "Settings" button on the toast to go to the host app's settings page to turn on the camera. Both the prompting message and button title can be customized, see Custom Strings for more details.
In the ContainerConfiguration
object, a new bitmask value requestCameraUsage
is supported by the enabledUIElements
property. The toast will display if you either leave this property unset or explicitly include the requestCameraUsage
bitmask in it. The toast will not display if you do not include this bitmask value in the property.
The following code snippet shows how to enable the camera usage request toast.
var config = ContainerConfiguration()
// Combine requestCameraUsage with other options.
config.enabledUIElements = [.requestCameraUsage, .cardListToast]
// Or do not assign values to 'enabledUiElements' at all.
The following code snippet shows how to disable the camera usage request toast.
var config = ContainerConfiguration()
// Do not include requestCameraUsage.
config.enabledUIElements = [.cardListHeader, .cardListToast]
Custom Strings
The configuration object also allows you to specify custom strings for messages and button titles using the setCustomValue(_:for:)
method on the configuration object:
processingStateMessage
: The message displayed on the upload processing overlay during file upload. Defaults to "Sending, please wait...".processingStateCancelButtonTitle
: The text displayed on the cancel button in the upload overlay during file upload. Defaults to "Cancel process".toastFileUploadFailedMessage
: Customised toast message shown when file(s) fail to upload during card submission. Defaults to "Couldn't upload file(s)".requestCameraAccessMessage
: Customised toast message shown when requesting camera access from the user. Defaults to "Access to your camera is required to take photos. Please enable camera access in your device settings".requestCameraAccessSettingsTitle
: The title for the button in the toast message prompting for camera access, which navigates to the Settings app. Defaults to "Settings".
Container Events
The SDK allows you to perform custom actions in response to denied camera usage events. To be notified when these occur, assign a card event delegate to your stream container.
When the user tries to take photo but the camera isn't available, the following two events could occur:
cameraDenied
: The user has explicitly denied an app permission to capture media. Equivalent to iOS's AVAuthorizationStatusDenied status.cameraRestricted
: The app isn’t permitted to use media capture devices. Equivalent to iOS's AVAuthorizationStatusRestricted status.
The following code snippet shows how to capture these events in the host app.
var config = ContainerConfiguration()
config.onEvent = { event in
switch event {
case .cameraDenied(let cardInstanceId), .cameraRestricted(let cardInstanceId):
// Show your own camera access request UI.
default:
break
}
}
SDK Events
There are three new event classes related to file upload: AACSDKEventUserFileUploadsStarted
, AACSDKEventUserFileUploadsFailed
, and AACSDKEventUserFileUploadsCompleted
, each containing an array of uploaded file info.
See Observing SDK events for more details of SDK event observer.
The following code snippet captures the started
event and prints out the number of files.
AACSession.observeSDKEvents { event in
switch event.eventType {
case .userFileUploadsStarted:
if let event = event as? AACSDKEventUserFileUploadsStarted {
print("Captured file upload event with \(event.fileInfo.count) files.")
}
default:
break
}
}
Currently, no events are generated when a user cancels an upload. We will add a corresponding SDK event in upcoming versions.
Third-party dependencies
Atomic iOS SwiftUI SDK contains one third-party dependency:
- Font Awesome Free 5.15.4, an icon font that is used for card icons. This is distributed as an OTF font.
Atomic iOS SwiftUI SDK does not use any dependency managers, all dependencies are integrated manually.
The standard Atomic iOS SDK is also included within SwiftUI SDK. For more information on its third-party dependencies, see the iOS SDK Guide.