Skip to main content

Remote Settings

The PAPulse SDK supports Remote Settings and A/B experiments, which are configured on the analytics web dashboard and distributed across all devices where your app is installed, using the Internet. For more information about user segmentation, filters, funnels, and A/B experiment configuration, please refer to the Fundamentals section.

The SDK establishes contact with the analytics server upon session initialization in order to retrieve the latest Remote Settings. This process is automated through the PAAnalytics.start() method call. Literally, Remote Settings is a big dictionary that uses key-value pairs to store the configured values.

By default, the "https://settings.picsart.com/api/settings" endpoint is used to fetch Remote Settings. However, you have the flexibility to modify this URL by making a specific call.

PAAnalytics.configSet(settingsURL: url)

Once the Remote Settings are fetched, the SDK caches them to be applied in the next app launch. This means that the Remote Settings your app is currently using were obtained during the previous app launch. These cached Remote Settings may be outdated compared to the Remote Settings fetched during the current session. This might seem unusual, but this approach is designed to protect your app.

When the SDK is initialized, the cached Remote Settings are immediately available for reading through the SDK's public API, allowing your app to utilize them. The retrieval of new Remote Settings occurs a few seconds later, depending on network conditions and the size of the Remote Settings. If the new Remote Settings start working in parallel with the cached ones, it can lead to unpredictable app behavior.

Nevertheless, you have the flexibility to choose whether to use the cached settings or apply the newly downloaded settings immediately.

You can retrieve the full dictionary of Remote Settings by using a specific method

PAAnalytics.settings

As well as to know Remote Settings type

PAAnalytics.settingsType

Type might be:

  • .default - If the PAPulse SDK does not currently have any Remote Settings, it is possible that this is the first launch of the app after installation.
  • .cache - The SDK uses local cached Remote Settings from the disk
  • .production - The SDK uses newly fetched Remote Settings from backend

In addition, the PAPulse SDK offers a set of convenient methods that enable you to retrieve values from the Remote Settings using a known key and the expected result type. The naming of the methods is self-explanatory, making it clear what their purpose is.

PAAnalytics.object(forKey: key)
PAAnalytics.string(forKey: key)
PAAnalytics.array(forKey: key)
PAAnalytics.dictionary(forKey: key)
PAAnalytics.integer(forKey: key)
PAAnalytics.float(forKey: key)
PAAnalytics.double(forKey: key)
PAAnalytics.bool(forKey: key)
PAAnalytics.url(forKey: key)

Each of these methods has a corresponding counterpart that accepts a default value. If the requested key is not found in the Remote Settings dictionary, the method with a default value parameter will return that default value instead.

You can initiate new Remote Settings fetching manually by calling

PAAnalytics.refreshSettings()

There's a callback to trigger some actions that allows you to be notified when new Remote Settings have been fetched and are ready to be applied.

callback_token = PAAnalytics.setSettingsCallback {
// your code here
}

To unsubscribe from settings callback triggering:

PAAnalytics.removeSettingsCallback(token: callback_token) {

To stop listening to Remote Settings updates and prevent immediate application of newly fetched Remote Settings, you can call a specific method. This action ensures that the SDK and the app continue to use the cached Remote Settings instead, without applying the newly fetched Remote Settings immediately.

PAAnalytics.stopListeningToSettingsChanges()

Exclusive Settings with tag

The PAPulse SDK provides a smart way to manage isolated, tag-based Remote Settings that are independent from the main settings system. This feature enables you to create separate settings contexts for different features, modules, or teams, each identified by a unique tag string.

Unlike traditional Remote Settings that apply globally across your app and work synchronously, Exclusive Settings allow:

  • Create multiple independent settings contexts using tags
  • Fetch asynchronously settings on-demand for specific features or experiments
  • Subscribe to real-time updates for specific tagged settings
  • Manage different expiration times and caching strategies per tag

Prefetching Settings

You can proactively fetch settings for a specific tag to ensure they're available when needed. This is particularly useful for critical features that require settings to be ready before user interaction.

PAAnalytics.prefetch(settingsForTag: "create_flow")

Subscribing to Settings Updates

Subscribe to real-time notifications when settings for a specific tag are updated. The callback receives a boolean indicating whether the update was successful.

let subscription = PAAnalytics.subscribe(settingsUpdate: "experiment_group_a") { success in
if success {
// Settings for "experiment_group_a" have been updated
// Refresh your UI or reconfigure feature behavior
}
}

Remember to keep a reference to the subscription object to maintain the subscription. The subscription will be automatically cancelled when the object is deallocated.

Retrieving Tagged Settings Values

The SDK provides both Swift async/await methods and Objective-C compatible callback-based methods for retrieving tagged settings values.

Swift Async/Await Methods

For Swift projects, you can use the modern async/await syntax:

// Get a value with a default fallback
let isFeatureEnabled: Bool = await PAAnalytics.get(
forKey: "feature_enabled",
tag: "feature_x",
default: false
)

// Get a value with timeout and default fallback
let maxRetries: Int = await PAAnalytics.get(
forKey: "max_retries",
tag: "network_config",
expectationTime: 2.0,
default: 3
)

// Get an optional value without default
let customURL: String? = await PAAnalytics.get(
forKey: "api_endpoint",
tag: "backend_config"
)

// Get an optional value with timeout
let timeout: Double? = await PAAnalytics.get(
forKey: "request_timeout",
tag: "network_config",
expectationTime: 1.5
)

Objective-C Compatible Callback Methods

For Objective-C projects or when you prefer callback-based approaches:

Generic Object Retrieval:

PAAnalytics.getObject(forKey: "config_data", tag: "feature_x") { result in
// Handle the result of type Any?
}

PAAnalytics.getObject(forKey: "config_data", tag: "feature_x", expectationTime: 2.0) { result in
// Handle the result with 2-second timeout
}

String Values:

PAAnalytics.getString(forKey: "welcome_message", tag: "onboarding") { message in
// Handle optional String result
}

PAAnalytics.getString(forKey: "welcome_message", tag: "onboarding", defaultValue: "Welcome!") { message in
// Handle non-optional String result with default
}

Numeric Values:

PAAnalytics.getInteger(forKey: "max_items", tag: "ui_config", defaultValue: 10) { count in
// Handle Integer result
}

PAAnalytics.getDouble(forKey: "animation_duration", tag: "ui_config", defaultValue: 0.3) { duration in
// Handle Double result
}

PAAnalytics.getBool(forKey: "dark_mode", tag: "theme_config", defaultValue: false) { isDarkMode in
// Handle Boolean result
}

Collection Types:

PAAnalytics.getArray(forKey: "menu_items", tag: "navigation") { items in
// Handle optional [Any] result
}

PAAnalytics.getDictionary(forKey: "theme_colors", tag: "ui_config", defaultValue: [:]) { colors in
// Handle [AnyHashable: Any] result with default
}

URL Values:

PAAnalytics.getURL(forKey: "api_base_url", tag: "backend_config") { url in
// Handle optional URL result
}

Expectation Time Parameter

The expectationTime parameter allows you to define how long (in seconds, as a TimeInterval) the SDK should wait for updated settings from the server before falling back to cached or default values. This parameter helps balance between data freshness and app responsiveness:

  • When expectationTime is nil:
    The SDK will wait indefinitely for a server response. However, if the request fails (e.g., due to a server error or lack of internet connectivity), the SDK will revert to using the cached or default value at the point where the asynchronous method was originally called.

  • When expectationTime is set:
    The SDK will wait up to the specified duration for fresh data. If the timeout is reached before receiving a response, the SDK will fall back to the cached or default value, and the ongoing network request for settings will be terminated.

Use this setting to control the trade-off between ensuring the most up-to-date configuration and maintaining a responsive user experience.

Pre-Connect

Usually when an user launches the application, a long sequence of 3rd libraries, including the SDK, is initiated. Depending on the device's performance, the entire initialization process can take up to 500ms. Following this, the SDK establishes a network connection, which in itself can consume up to hundreds of milliseconds. Consequently, a significant delay occurs before the remote configuration request can be made. To enhance the user experience, there is an opportunity to reduce this latency during the initial phases of the app's launch, thereby expediting the retrieval of remote settings and decreasing application startup time.

To achieve this objective, the SDK efficiently leverages an existing connection from the iOS connection pool through a mechanism known as pre-connect.

Activation of the pre-connect feature is initiated by sending an HTTP request containing the application name and platform name to the pre-connect endpoint. The server consistently responds with an HTTP 204 status code. Subsequently, the time spent on the next actual request to fetch remote settings is reduced because certain steps, such as the SSL handshake and secure connection establishment, have already been completed during the preceding pre-connect request.

The SDK offers a method that the application can invoke to initiate the pre-connect process. It is advisable to call this method as early as possible, even prior to configuring and initializing the SDK.

PAAnalytics.preconnectSettings(url:<YOUR_ASSIGNED_PRE-CONNECT_URL>)

Background assets (iOS 16+)

Apple has introduced a new BackgroundAssets framework in iOS 16 that enables the fetching of resources from the backend after app installation, but before its first launch. The Pulse Analytics SDK has adopted this feature for iOS 16+ to help fetch initial Remote Settings for your app before its first launch, thereby improving the UI/UX for the users. To apply this feature, you need to create a Background Download App Extension and call the necessary methods from PAExtensionAnalytics module.

Get an URL request to prepare a BAURLDownload instance as a result of func downloads(for:manifestURL:extensionInfo) -> Set<BADownload> method of BADownloaderExtension protocol

let remoteSettingsAsset = PAExtensionAssets(groupName: <YOUR_APP_GROUP>, appName: <YOUR_APP_NAME>, keychainGroup: <YOUR_KEYCHAIN_GROUP>)
let urlRequest = remoteSettingsAsset.remoteSettingsURLRequest()

Save downloaded Remote Settings configuration temporary file from the backgroundDownload:finishedWithFileURL callback for future reading by the Pulse Analytics SDK at the first launch of your app

remoteSettingsAsset.saveRemoteSettingsData(fromFile: url)

In addition, to support this feature, you need to add additional parameters to the Info.plist file of your app as

<key>PAPulseSDK</key>
<dict>
<key>AppGroup</key>
<string>YOUR_APP_GROUP</string>
<key>KeychainGroup</key>
<string>YOUR_SHARED_KEYCHAIN</string>
</dict>

Sample of Background Download App Extension for iOS 16+

import BackgroundAssets
import PAPulse
import os.log

/** You can use `backgroundassets-debug` MasOS console command (see `man backgroundassets-debug`)
in order to emulate waking up this App Extension and verify Background Assets fetching for
life cycle events like "app-install", "app-update", "app-periodic-check".
Example: "xcrun backgroundassets-debug -l" - get list of connected iOS devices( and their
ids as well)
Example: "xcrun backgroundassets-debug -s --app-install -b "com.picsart.studiobeta" -d
<YOUR_IOS_DEVICE_ID>"
*/

@main
struct BackgroundDownloadHandler: BADownloaderExtension {
typealias ResultHandler = (URL) -> Result<String?, Error>

private enum BackgroundDownloadError: Error {
case invalidDataInTempFile
}

private static var handlers = [String: ResultHandler]()

func downloads(
for reason: BAContentRequest,
manifestURL _: URL,
extensionInfo _: BAAppExtensionInfo
) -> Set<BADownload> {
guard let appName = Bundle.main.infoDictionary?["AppName"] as? String else {
return []
}

guard let appGroup = Bundle.main.infoDictionary?["AppGroup"] as? String else {
return []
}

guard let keychainGroup = Bundle.main.infoDictionary?["KeychainGroup"] as? String else {
return []
}

guard reason == .install || reason == .update else {
return []
}

var downloadsToSchedule: Set<BADownload> = []

if let download = requestToFetchRemoteSettings(appName: appName, appGroup: appGroup,
keychainGroup: keychainGroup ) {
downloadsToSchedule.insert(download)
}

return downloadsToSchedule
}

func backgroundDownload(_ download: BADownload, failedWithError error: Error) {
Self.handlers.removeValue(forKey: download.identifier)
}

func backgroundDownload(
_ download: BADownload,
finishedWithFileURL fileURL: URL
) {
defer {
if FileManager.default.fileExists(atPath: fileURL.path) {
try? FileManager.default.removeItem(at: fileURL)
}
}

let functionName = "\(#function) for \(download.identifier)"

if let handler = Self.handlers[download.identifier] {
let result = handler(fileURL)
}

Self.handlers.removeValue(forKey: download.identifier)
}

func extensionWillTerminate() {
Self.handlers.removeAll()
}

// MARK: - Private

private func isDebugModeEnabled(appGroup: String) -> Bool {
// DemoApp will set DebugModeEnabled forever
true
}

private func requestToFetchRemoteSettings(
appName: String,
appGroup: String,
keychainGroup: String
) -> BAURLDownload? {
let remoteSettingsAsset = extensionAssetForRemoteSettings(appName: appName,
appGroup: appGroup, keychainGroup: keychainGroup)

guard let urlRequest = remoteSettingsAsset.remoteSettingsURLRequest() else {
return nil
}

let id = remoteSettingsAsset.requestId
let download = BAURLDownload(identifier: id,
request: urlRequest,
applicationGroupIdentifier: appGroup,
priority: .max)

Self.handlers[id] = { url in
let result = remoteSettingsAsset.saveRemoteSettingsData(fromFile: url)
if let error = result.failure {
return .failure(error)
} else {
return .success(result.success)
}
}

return download
}

private func extensionAssetForRemoteSettings(
appName: String,
appGroup: String,
keychainGroup: String
) -> PAExtensionAssets {
let remoteSettingsAsset = PAExtensionAssets(groupName: appGroup, appName: appName,
keychainGroup: keychainGroup)

if let info = Bundle.main.infoDictionary {
remoteSettingsAsset.appVersion = info["CFBundleShortVersionString"] as? String
remoteSettingsAsset.appVersionCode = info["CFBundleVersion"] as? String
}

return remoteSettingsAsset
}
}