This documentation is for the Countly Swift SDK version 26.8.X. The SDK source code repository can be found here.
The Countly Swift SDK supports iOS and iPadOS 15.0, tvOS 15.0, watchOS 9.0, macOS 12.0, and visionOS 1.0 or later. It is distributed as a Swift package and needs Swift 5.9 or later.
Applications written in Objective-C use the same SDK. See Using the SDK from Objective-C.
If your application already uses the Countly iOS SDK, the Swift SDK picks up the device ID, the cached remote config, and the queued requests it left behind. See Moving from the iOS SDK.
If you are in doubt about the correctness of your Countly SDK integration, you can learn about the verification methods from here.
Adding the SDK to the Project
The SDK is added with Swift Package Manager. In Xcode, go to File > Add Package Dependencies, enter https://github.com/Countly/countly-sdk-swift.git, and add the Countly library to your app target. In a Package.swift manifest, add the package to your dependencies:
.package(url: "https://github.com/Countly/countly-sdk-swift.git", from: "26.8.0")Then add .product(name: "Countly", package: "countly-sdk-swift") to the dependencies of your target.
The package has a second library, CountlyNotificationService, for the notification service extension that shows rich push notifications. See Rich Media.
SDK Integration
Minimal Setup
Import Countly, create a CountlyConfig with your app key and server URL, and start the SDK as early as possible, on the main thread:
import Countly
let config = CountlyConfig()
config.appKey = "YOUR_APP_KEY"
config.host = "https://YOUR_COUNTLY_SERVER"
Countly.shared.start(with: config)In a UIKit application, start the SDK in application(_:didFinishLaunchingWithOptions:). In a SwiftUI application, start it in the initializer of your App:
import SwiftUI
import Countly
@main
struct MyApp: App {
init() {
let config = CountlyConfig(appKey: "YOUR_APP_KEY", host: "https://YOUR_COUNTLY_SERVER")
Countly.shared.start(with: config)
}
var body: some Scene {
WindowGroup { Text("Hello") }
}
}That is the whole integration. Sessions, device metrics, and the device ID are handled from there. The app key is on Management > Applications in Countly, and the server URL is the address you open Countly at.
Starting the SDK copies the config, so changing the CountlyConfig object afterwards has no effect on the running SDK.
Every feature is reached through its own property on the instance:
Countly.shared.events.recordEvent("purchase", segmentation: ["item": "hat"], count: 1, sum: 9.99)
if let viewID = Countly.shared.views.startView("Checkout") {
Countly.shared.views.stopView(id: viewID)
}
Countly.shared.userProfile.setProperties(["name": "Ada", "email": "ada@example.com"])
Countly.shared.userProfile.save()Additional Features
Automatic crash reporting and push notifications are switched on with features before start:
config.features = [.crashReporting, .pushNotifications]Moving from the iOS SDK
Countly.shared, the default instance, reads and writes the same storage the Countly iOS SDK used. An application that replaces the iOS SDK with the Swift SDK keeps its device ID, its cached remote config values, and the requests still waiting to be sent, which move into the Swift SDK's own database on the first start.
An application that goes back to the iOS SDK after a release with the Swift SDK takes the queued requests along when it adds Migration/CountlySwiftSDKMigration.m from the repository to its app target. There is nothing to call: on the first launch it moves the queue to where the iOS SDK reads it. Without the file nothing is lost either: the requests wait until the application comes back to the Swift SDK.
SDK Data Storage
The SDK keeps everything on the device until the server accepts it, so nothing recorded is lost when the application is closed, killed, or offline. It uses two places:
-
An SQLite database for the request queue:
CountlyRequests.sqlite, with the-waland-shmfiles SQLite keeps next to it. It holds every request waiting to be sent, crash reports included, up tostoredRequestsLimitrequests (1000 by default). Each change is written as one transaction. -
UserDefaultsfor everything else: the device ID, the events recorded but not yet moved into a request (at most 1000), the remote config values, the SDK behavior settings, the push token state, and the health check counters.
The database is in the Application Support directory. On macOS it is in a folder named after the bundle identifier, and on tvOS it is in the Caches directory, the only directory tvOS guarantees to be writable. A named instance has its own database file, with the instance's name in the file name, and its own UserDefaults keys.
The database survives problems on its own:
- A database that cannot be opened, such as a protected file on a locked device, is left alone and opened later.
- A corrupt database is set aside with a
.corruptextension and a new one is started. - If the application crashes while a change is being written, the queue goes to
CountlyRequests.pendinginstead, and the next launch adds it back.
The Countly iOS SDK keeps its queue in a file named Countly.dat. On the first launch after you move to the Swift SDK, its requests are added to the database and the file is removed. See Moving from the iOS SDK.
Multiple Instances
Several instances can run side by side. Each one owns its storage, device ID, request queue, timers, and log level, so one application can report to two app keys or two servers at once without them seeing each other's data:
let analyticsConfig = CountlyConfig(appKey: "SECOND_APP_KEY", host: "https://YOUR_COUNTLY_SERVER")
let analytics = Countly.instance(named: "analytics")
analytics.start(with: analyticsConfig)
analytics.events.recordEvent("opened_from_widget")-
Countly.instance(named:)creates the instance if needed but never starts it, so holding a reference cannot begin collecting data by accident. An empty name is refused. -
Countly.getInstance(named:)returns an existing instance ornil, andCountly.listInstances()returns the names in use. -
halt(clearStorage:)stops an instance and keeps it registered, so it can be started again. WithclearStorage: trueit erases everything the instance stored. -
Countly.removeInstance(named:)stops a named instance, drops it from the registry, and keeps its stored data. - Push notifications belong to the default instance only, because the device token and the notification taps belong to the whole process.
Using the SDK from Objective-C
The interface is available to Objective-C under the same names, apart from the SwiftUI countlyView modifier:
@import Countly;
CountlyConfig *config = [[CountlyConfig alloc] initWithAppKey:@"YOUR_APP_KEY" host:@"https://YOUR_COUNTLY_SERVER"];
config.consents = @[@(ConsentFeatureSessions), @(ConsentFeatureEvents)];
[Countly.shared startWithConfig:config];
[Countly.shared.events recordEvent:@"purchase" segmentation:@{@"item": @"hat"}];
[Countly.shared.views startView:@"Checkout" segmentation:nil];Swift default arguments do not exist in Objective-C, so pass nil for an argument you would leave out in Swift. A value Swift keeps as an optional enum or an array of enums, such as config.pushTestMode or config.consents, is an NSNumber holding the enum value.
SDK Logging
Set enableDebug to see the SDK's internal log in the Xcode console. internalLogLevel picks the lowest level printed, debug by default:
config.enableDebug = true
config.internalLogLevel = .verboseThe levels are .error, .warning, .info, .debug, and .verbose. disableSDKLoggingInProduction keeps the console quiet in release builds even when enableDebug is on.
Logger Delegate
To receive the log lines yourself, for example to forward them to your own logging, set a loggerDelegate. It gets every line at the configured level, also when enableDebug is off:
final class LogForwarder: NSObject, CountlyLoggerDelegate {
func internalLog(_ log: String, level: CountlyLogLevel) {
print("Countly", level.rawValue, log)
}
}let forwarder = LogForwarder()
config.loggerDelegate = forwarderThe config holds the delegate weakly, so keep a strong reference to it for as long as the SDK runs.
SDK Log Gathering
If a user reports a problem you cannot reproduce, you can ask the SDK on that user's device to send its own log lines to your Countly server. Start log gathering from the user's profile in Countly (Open SDK Debugger in the profile's menu) and pick the log levels to collect. The request reaches the SDK with the SDK behavior settings, so gathering starts the next time the SDK fetches them on that device. The lines are uploaded in batches, also when logging is disabled in the app. When consent is required, a line is uploaded only after the consent of the feature that wrote it is given.
Connection Test
When data from a user does not reach your server, a connection test shows which Countly endpoints that user's device can reach. Request it from the user's profile in Countly (Open SDK Debugger, then Connection test). The SDK picks the request up with its next SDK behavior settings fetch, checks every endpoint it depends on, and reports for each one whether it answered, with the HTTP status and the response time. The report needs no consent.
Crash Reporting
Automatic Crash Handling
Add .crashReporting to features to catch uncaught exceptions and fatal signals:
config.features = [.crashReporting]A crash is written to storage while the process goes down and sent on the next launch, so nothing is sent from inside the crash handler. The SDK chains to any handler installed before it, and a signal the application ignores, such as SIGPIPE, is left alone.
A named instance installs the automatic handlers only when its own features includes crash reporting.
Recording Handled Errors
Report an error you caught with recordError. A Swift error is reported with its type as the crash type, and an NSError with its domain:
enum StorageError: Error {
case diskFull
}
do {
throw StorageError.diskFull
} catch {
Countly.shared.crashes.recordError(error, segmentation: ["screen": "settings"])
}An error known only by name, an NSException, a stack trace of your own, and fatal errors are recorded too:
Countly.shared.crashes.recordError("Payment failed", isFatal: false, stackTrace: Thread.callStackSymbols)
let exception = NSException(name: .genericException, reason: "Unexpected state", userInfo: nil)
Countly.shared.crashes.recordException(exception, isFatal: false, segmentation: ["build": "beta"])Without a stack trace, the SDK records the stack of the calling thread.
Crash Breadcrumbs
Breadcrumbs are short log lines sent with the next crash report, describing what the user did before it:
Countly.shared.crashes.addCrashBreadcrumb("Opened the checkout screen")
Countly.shared.crashes.clearCrashBreadcrumbs()At most 100 breadcrumbs are kept, the oldest one dropped first. sdkInternalLimits.maxBreadcrumbCount changes the limit.
Custom Crash Segmentation
Segmentation given at start, or set later, is sent with every crash report:
config.crashSegmentation = ["flavor": "paid"]Countly.shared.crashes.setCrashSegmentation(["flavor": "paid", "theme": "dark"])Segmentation passed with a single report wins over both.
Crash Filtering
Set crashes.crashFilterCallback to inspect each report before it is sent. Return true to drop the report. The report can also be changed in place, for example to remove sensitive data:
config.crashes.crashFilterCallback = { crash in
if crash.crashDescription.contains("SECRET") {
return true
}
crash.crashSegmentation["filtered"] = true
return false
}The callback gets the stack trace, the name, the description, the breadcrumbs, the segmentation, and the device metrics of the report.
Symbolication
Crash reports carry the binary images, the build UUID, and the CPU architecture the server needs to symbolicate them. Upload the dSYM file of each build as described in the iOS SDK documentation, then symbolicate reports in Countly as described here.
Events
Recording Events
An event has a key and, optionally, segmentation, a count, a sum, and a duration in seconds:
let events = Countly.shared.events
events.recordEvent("level_finished")
events.recordEvent("purchase", segmentation: ["item": "hat", "price_tier": 2], count: 1, sum: 9.99)
events.recordEvent("video_watched", segmentation: ["video": "intro"], duration: 42)Segmentation values can be strings, numbers, booleans, dates, or arrays of these. A count below one is recorded as 1, and a sum or duration that is not a finite number, or a negative duration, as 0, with a warning in the log.
Events are held in memory and in storage and sent in batches: after eventSendThreshold events (100 by default), with every session update, and when the application goes to the background.
Timed Events
A timed event measures its own duration from start to end:
let events = Countly.shared.events
events.startEvent("checkout")
events.endEvent("checkout", segmentation: ["payment": "card"], count: 1, sum: 24.5)
events.startEvent("tutorial")
events.cancelEvent("tutorial")Starting an event that is already running, or ending or cancelling one that is not, is ignored with a warning.
Sessions
Automatic Session Tracking
By default the SDK begins a session when the application comes to the foreground, updates it every updateSessionPeriod seconds, and ends it when the application goes to the background. On macOS the session also ends when the application hides or quits, and the next activation begins a new one. On watchOS the session follows the extension's active state.
Session durations come from a monotonic clock, so changing the device's date and time cannot stretch or shrink a session.
Manual Sessions
Set manualSessionHandling to control sessions yourself:
config.manualSessionHandling = trueCountly.shared.sessions.beginSession()
Countly.shared.sessions.updateSession()
Countly.shared.sessions.endSession()Events recorded inside a session are sent before it ends. Beginning a session that is running, or updating or ending one that is not, is ignored with a warning.
Hybrid Mode
With enableManualSessionControlHybridMode you begin and end sessions yourself while the SDK sends the periodic updates:
config.manualSessionHandling = true
config.enableManualSessionControlHybridMode = trueUpdate Session Period
updateSessionPeriod is the interval of session updates and event batches, 60 seconds by default and 20 seconds on watchOS:
config.updateSessionPeriod = 30Content Zone
The content zone shows content made with the content builder in Countly, such as announcements and journey steps. Content needs a web view, so it is available on iOS, visionOS, and macOS. On tvOS and watchOS these calls are ignored and nothing is fetched.
Countly.shared.content.enterContentZone()After entering, the SDK asks the server for content at the start and then every content.zoneTimerInterval seconds, and shows what it gets as an overlay. Leave the zone when content should no longer appear:
Countly.shared.content.exitContentZone()refreshContentZone() asks again as soon as everything queued has reached the server, and previewContent(_:) shows a single content by its 24 character ID, for checking a content before you publish it.
Content is not shown in temporary device ID mode or while disableWebView is set, and the SDK asks for nothing while a content or a feedback widget is already on screen.
Fine Tuning Content
zoneTimerInterval takes 30 seconds by default and 15 seconds or more. After a content closes, the next fetch waits two intervals:
config.content.zoneTimerInterval = 60Content Callback
globalContentCallback tells you when a content was completed or closed, with the data the content sent:
config.content.globalContentCallback = { status, data in
switch status {
case .completed:
print("Content completed", data)
case .closed:
print("Content closed", data)
@unknown default:
break
}
}Deep Links
Content can open links. To handle a link yourself, set contentURLHandler and return true when you handled it. The links you do not handle are opened by the SDK. By default every scheme is allowed except the dangerous ones, such as javascript and file, which are never opened. To allow only some schemes, list them in allowedLinkSchemes:
config.content.allowedLinkSchemes = ["https", "myapp"]
config.content.contentURLHandler = { url in
guard url.scheme == "myapp" else { return false }
print("Open", url)
return true
}Display Options
webViewDisplayOption decides whether content covers the whole screen (.immersive) or stays inside the safe area (.safeArea). overlayCornerRadius rounds the overlay's corners:
config.content.webViewDisplayOption = .safeArea
config.content.overlayCornerRadius = 12Reloading Content on Stall
With enableContentReloadOnStall, a content that has not finished loading after contentReloadOnStallTimeout milliseconds (1000 by default) is loaded again:
config.content.enableContentReloadOnStall = true
config.content.contentReloadOnStallTimeout = 2000Disabling Zoom and Rotation
config.content.disableZoom = true
config.content.disableRotation = truedisableZoom stops the user from zooming content, and disableRotation keeps a content in the orientation it was shown in.
Global Content Segmentation
Segmentation set here is recorded with every content and feedback widget event:
Countly.shared.content.setGlobalContentSegmentation(["campaign": "spring"])View Tracking
Automatic Views
On iOS, tvOS, and visionOS, enableAutomaticViewTracking reports every view controller that appears, by its title or class name. A SwiftUI hosting controller is reported under the name of the view it hosts:
config.enableAutomaticViewTracking = trueAutomatic View Exceptions
Exclude screens by their reported name, their class name, or a superclass, at start or later:
config.automaticViewTrackingExclusionList = ["SplashViewController", "Onboarding"]Countly.shared.views.addAutoViewTrackingExclusionList(["DebugMenu"])A view controller can name itself by adopting CountlyAutoViewTrackingName. disableSwizzling turns automatic view tracking off for its instance.
SwiftUI Views
A SwiftUI screen names itself with the countlyView modifier, which reports the view when it appears and stops it when it disappears. It works with automatic view tracking on or off:
import SwiftUI
import Countly
struct CheckoutView: View {
var body: some View {
Text("Checkout")
.countlyView("Checkout", segmentation: ["step": 2])
}
}For a named instance, pass it: countlyView("Checkout", instance: analytics).
Manual View Recording
Auto Stopped Views
An auto stopped view ends when the next one starts:
Countly.shared.views.startAutoStoppedView("Home", segmentation: ["tab": "featured"])Regular Views
A regular view runs until you stop it, so several can run at once. Starting one returns its ID:
let viewID = Countly.shared.views.startView("Product", segmentation: ["sku": "A-12"])Stopping Views
let views = Countly.shared.views
if let viewID = views.startView("Product") {
views.stopView(id: viewID, segmentation: ["scrolled": true])
}
views.stopView(name: "Home")
views.stopAllViews(segmentation: nil)Of several open views with one name, stopping by name stops the last one started.
Pausing and Resuming Views
let views = Countly.shared.views
if let viewID = views.startView("Video") {
views.pauseView(id: viewID)
views.resumeView(id: viewID)
}A paused view stops counting time until it resumes. Every running view stops when the application goes to the background and starts again as a new visit when it returns.
Adding Segmentation to Started Views
let views = Countly.shared.views
if let viewID = views.startView("Search") {
views.addSegmentation(toViewWithID: viewID, segmentation: ["results": 12])
}
views.addSegmentation(toViewWithName: "Search", segmentation: ["filter": "price"])Global View Segmentation
Global view segmentation is added to every view. Set it at start, replace it, or merge new values into it:
config.globalViewSegmentation = ["app_flavor": "pro"]Countly.shared.views.setGlobalViewSegmentation(["app_flavor": "pro"])
Countly.shared.views.updateGlobalViewSegmentation(["theme": "dark"])Segmentation given to a single view wins over the global values.
Device ID Management
The SDK generates a device ID on the first start and keeps it. To use your own, set deviceID before start:
config.deviceID = "user-1234"Changing Device ID
setID picks the merge behavior for you: an ID the SDK generated is merged into the new one on the server, and an ID you supplied is not. To choose yourself, call one of the explicit methods:
let deviceID = Countly.shared.deviceID
deviceID.setID("user-1234")
deviceID.changeWithMerge("user-1234")
deviceID.changeWithoutMerge("user-5678")Merging keeps the user's history under the new ID, which suits a user logging in. A change without merge starts a new user: the SDK first sends what belongs to the leaving user, ends the session, and removes the consent given for them.
Temporary Device ID
In temporary device ID mode the SDK keeps recording but sends nothing until a real device ID is set, so a user who is not identified yet does not create a profile of their own:
config.temporaryDeviceIDMode = trueCountly.shared.deviceID.enableTemporaryIDMode()
Countly.shared.deviceID.disableTemporaryIDMode("user-1234")The requests held back go out under the device ID set when the mode ends. Without an ID, the SDK returns to the device ID used before the mode began.
Retrieving Current Device ID
let current = Countly.shared.deviceID.current
let type = Countly.shared.deviceID.type
let isTemporary = Countly.shared.deviceID.isTemporaryIDMode
print(current ?? "-", type.map { "\($0)" } ?? "-", isTemporary)The type is .developerSupplied, .sdkGenerated, or .temporary.
Resetting Stored Device ID
resetStoredDeviceID drops the stored device ID at start, so the configured one, or a new generated one, is used:
config.resetStoredDeviceID = truePush Notifications
Push notifications work on iOS, visionOS, and macOS, through the default instance. Before integrating, set up your credentials in Countly as described here.
Integration
In Xcode, add the Push Notifications capability and the Remote notifications background mode to your app target. Then add .pushNotifications to features and set the test mode of the build:
config.features = [.pushNotifications]
config.pushTestMode = .developmentUse .development for builds run from Xcode, .testFlightOrAdHoc for TestFlight and ad hoc builds, and leave it nil for App Store builds.
With push on, the SDK claims the two remote notification registration callbacks of your application delegate and the UNUserNotificationCenter delegate, and always calls your own implementations afterwards. The device token is reported, a tapped notification records its action event, and its link is opened.
Enabling Push
Ask the user for permission when it suits your application:
Countly.shared.push.askForNotificationPermission()To choose the notification types, pass UNAuthorizationOptions raw values, and receive the answer in a callback:
let options = UNAuthorizationOptions([.alert, .badge, .sound]).rawValue
Countly.shared.push.askForNotificationPermission(options: options) { granted, error in
print("Push permission", granted, error as Any)
}Add .provisional to the options to get provisional permission without a prompt; such notifications arrive quietly in the Notification Center.
Manually Handling Notifications
Set disableAutomaticPushHandling to keep the SDK out of both delegates, and forward the callbacks yourself:
config.disableAutomaticPushHandling = truefinal class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Countly.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
Countly.didFailToRegisterForRemoteNotifications(error: error)
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
Countly.handleNotificationResponse(response)
completionHandler()
}
}A tap handed over before the SDK started is kept and recorded at start. To record a button tap from a notification you show yourself, call recordAction(for:clickedButtonIndex:).
Rich Media
Action buttons and media attachments need a notification service extension. Add a Notification Service Extension target, link the CountlyNotificationService library to it, and forward the request:
import UserNotifications
import CountlyNotificationService
final class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
CountlyNotificationService.didReceive(request, withContentHandler: contentHandler)
}
}The extension keeps your own notification categories and downloads the attachment before the notification is shown.
Deep Links
A tapped notification opens its link. pushActionHandler sees the link first and can take it over by returning true. By default every scheme is allowed except the dangerous ones, which are never opened. To allow only some schemes, list them in pushAllowedLinkSchemes. The action is recorded either way:
config.pushAllowedLinkSchemes = ["https", "myapp"]
config.pushActionHandler = { link, userInfo in
guard let link, link.scheme == "myapp" else { return false }
print("Handle", link, userInfo.count)
return true
}Advanced Setup
Disabling Alerts Shown by Notifications
By default a notification that arrives while the application is in the foreground is shown as a banner. doNotShowAlertForNotifications turns that off.
config.doNotShowAlertForNotifications = trueAlways Sending Push Tokens
By default the token is sent once the user allowed notifications. sendPushTokenAlways sends it whatever the permission, so the server holds it if permission arrives later:
config.sendPushTokenAlways = trueSending or Clearing the Token
Countly.shared.push.recordPushNotificationToken()
Countly.shared.push.clearPushNotificationToken()Clearing sends an empty token, so the server stops sending notifications to the device.
macOS Launch Notification
On macOS, pass the user info of the launch notification to launchNotification, so a tap that launched the application is recorded:
func applicationDidFinishLaunching(_ notification: Notification) {
let config = CountlyConfig(appKey: "YOUR_APP_KEY", host: "https://YOUR_COUNTLY_SERVER")
config.features = [.pushNotifications]
config.launchNotification = notification.userInfo
Countly.shared.start(with: config)
}The SDK reports the same token twice within a minute only once, and a failed registration sends nothing, so the token the server holds stays.
User Location
Setting Location
By default the server works out the user's location from the IP address. To set it yourself, pass the values at start or later. They go with the next session start, or on their own when no session is coming:
config.location = CLLocationCoordinate2D(latitude: 38.42, longitude: 27.14)
config.city = "Izmir"
config.isoCountryCode = "TR"
config.ipAddress = "192.0.2.1"Countly.shared.location.recordLocation(CLLocationCoordinate2D(latitude: 38.42, longitude: 27.14),
city: "Izmir",
isoCountryCode: "TR",
ipAddress: nil)Disabling Location
Disabling location erases the location stored on the server and stops it from being inferred:
config.disableLocation = trueCountly.shared.location.disableLocationInfo()Remote Config
Remote config lets you change your application's behavior from Countly without a new release. Values are stored on the device until replaced or cleared.
Downloading Values
Automatic Remote Config Triggers
With enableRemoteConfigAutomaticTriggers, the SDK downloads every value at start, after a device ID change, and when consent for remote config is given:
config.enableRemoteConfigAutomaticTriggers = true
config.enableRemoteConfigValueCaching = trueenableRemoteConfigValueCaching keeps the values a later download did not return, instead of clearing them.
Manual Calls
let remoteConfig = Countly.shared.remoteConfig
remoteConfig.downloadKeys { result, error, fullValueUpdate, values in
print(result == .success, error as Any, fullValueUpdate, values.keys)
}
remoteConfig.downloadSpecificKeys(["button_color", "welcome_text"])
remoteConfig.downloadOmittingKeys(["large_payload"])The result is .success, .networkIssue, or .error. Empty keys are ignored, and a list with no usable key downloads every value.
Accessing Values
let remoteConfig = Countly.shared.remoteConfig
let color = remoteConfig.getValue("button_color")
if let value = color.value as? String, color.isCurrentUsersData {
print("Use", value)
}
let all = remoteConfig.getAllValues()
print(all.count)isCurrentUsersData is false when the value was downloaded for a different user than the current one.
Clearing Stored Values
Countly.shared.remoteConfig.clearAll()Global Download Callbacks
Register callbacks to hear about every download, at start or later:
config.remoteConfigRegisterGlobalCallback { result, error, fullValueUpdate, values in
print("Downloaded", values.count)
}let remoteConfig = Countly.shared.remoteConfig
let token = remoteConfig.registerDownloadCallback { result, _, _, _ in
print(result == .success)
}
if let token {
remoteConfig.removeDownloadCallback(token)
}
remoteConfig.removeAllDownloadCallbacks()A/B Testing
A/B testing assigns a user to an experiment variant through remote config keys. Enrolling tells the server the user takes part.
Enrollment on Download
config.enrollABOnRCDownload = trueEnrollment on Access
let value = Countly.shared.remoteConfig.getValueAndEnroll("button_color")
let values = Countly.shared.remoteConfig.getAllValuesAndEnroll()
print(value.value as Any, values.count)Enrollment on Action
Countly.shared.remoteConfig.enrollIntoABTests(forKeys: ["button_color"])Exiting A/B Tests
Countly.shared.remoteConfig.exitABTests(forKeys: ["button_color"])An empty list exits every A/B test.
Testing Variants
For testing, you can download the experiments and their variants and enroll into a variant of your choice:
let remoteConfig = Countly.shared.remoteConfig
remoteConfig.testingDownloadVariantInformation { result, error in
print(remoteConfig.testingGetAllVariants())
}
remoteConfig.testingEnrollIntoVariant(key: "button_color", variantName: "Variant A") { result, error in
print(result == .success)
}User Feedback
Feedback widgets (surveys, NPS, and ratings) are made in Countly and shown in a web view, so they are available on iOS, visionOS, and macOS. Learn more about them here.
Feedback Widgets
Show a widget by its ID, name, or tag. Without one, the first widget of that type is shown:
let feedback = Countly.shared.feedback
feedback.presentNPS()
feedback.presentSurvey("onboarding")
feedback.presentRating("app_rating") { state in
switch state {
case .appeared: print("Shown")
case .closed: print("Closed")
case .failed: print("Could not be shown")
@unknown default: break
}
}To pick a widget yourself, get the list of widgets available to the user and present one:
Countly.shared.feedback.getAvailableFeedbackWidgets { widgets, error in
guard let widget = widgets?.first(where: { $0.type == .survey }) else { return }
widget.present { state in
print(state.rawValue)
}
}Manual Reporting
To show the questions in your own interface, get the widget's data and record the answer. An NPS answer needs a whole number rating from 0 to 10, and a rating from 1 to 5:
Countly.shared.feedback.getAvailableFeedbackWidgets { widgets, _ in
guard let widget = widgets?.first(where: { $0.type == .nps }) else { return }
widget.getWidgetData { data, error in
widget.recordResult(["rating": 9, "comment": "Great"])
}
}Recording nil reports the widget as closed without an answer.
Star Rating
The legacy rating widget can be reported with your own interface. It needs a rating from 1 to 5 and the star rating consent:
Countly.shared.feedback.recordRatingWidget(id: "WIDGET_ID", rating: 4, email: "user@domain.com", comment: "Nice", userCanBeContacted: true)User Profiles
User properties describe the user. They are collected locally and sent with save(), at a session start or update, and before a device ID change:
Default User Properties
let profile = Countly.shared.userProfile
profile.setProperties([
"name": "Ada L.",
"username": "ada",
"email": "ada@domain.com",
"organization": "Countly",
"phone": "+15555555555",
"gender": "F",
"byear": 1990,
"picture": "https://domain.com/ada.png"
])
profile.save()On iOS and visionOS, picturePath with a local file path uploads the image.
Custom User Properties
let profile = Countly.shared.userProfile
profile.setProperty("plan", value: "premium")
profile.setProperties(["level": 12, "beta_tester": true])
profile.unsetCustomProperty("old_flag")
profile.save()A key that is not a default property is stored as a custom one. To clear a default property, set it to an empty string.
Custom User Property Modifiers
let profile = Countly.shared.userProfile
profile.increment("launch_count")
profile.incrementBy("coins", value: 50)
profile.multiply("score", value: 1.5)
profile.max("best_score", value: 980)
profile.min("best_time", value: 42.3)
profile.setOnce("first_seen_screen", value: "home")
profile.push("tags", value: "explorer")
profile.pushUnique("badges", value: "early_bird")
profile.pull("tags", value: "newbie")
profile.save()clear() drops the changes not saved yet.
Providing Initial User Properties
Properties given at start are sent with the first session:
config.providedUserProperties = ["plan": "free", "name": "Ada L."]Orientation Tracking
The SDK records the device orientation when it changes. enableOrientationTracking turns it off:
config.enableOrientationTracking = falseUser Consent
With requiresConsent, nothing is collected until the user gives consent for a feature:
config.requiresConsent = trueFeature Names
| Feature | What it covers |
|---|---|
.sessions |
Session tracking |
.events |
Events |
.viewTracking |
Views |
.userDetails |
User properties |
.crashReporting |
Crash reports and handled errors |
.pushNotifications |
Push tokens and push actions |
.location |
Location |
.attribution |
Attribution |
.remoteConfig |
Remote config and A/B testing |
.feedback |
Feedback widgets |
.starRating |
The legacy star rating |
.content |
The content zone |
.metrics |
Device metrics |
Setup During Init
config.requiresConsent = true
config.consents = [.sessions, .events, .viewTracking]enableAllConsents gives every consent at start.
Changing Consent
let consent = Countly.shared.consent
consent.giveConsent(for: [.sessions, .events])
consent.cancelConsent(for: .location)
consent.giveAllConsents()
consent.cancelAllConsents()
print(consent.hasConsent(for: .events))The consent state is sent to the server only when a call changed something. Withdrawing a consent stops the feature at once: withdrawing views closes every open view, and withdrawing push sends an empty token.
Feature Groups
let consent = Countly.shared.consent
consent.createFeatureGroup("analytics", features: [.sessions, .events, .viewTracking])
consent.setConsentFeatureGroup("analytics", given: true)
print(consent.hasConsent(forGroup: "analytics"))The server can lift or require consent with SDK behavior settings, which wins over the configuration while the settings contain it.
Security and Privacy
Parameter Tamper Protection
Set the same salt in Countly (Management > Applications, Salt for checksum) and in secretSalt. Every request then carries a checksum the server checks:
config.secretSalt = "YOUR_SALT"SSL Certificate Pinning
Add the certificate of your server to your application bundle and list its file name in pinnedCertificates. Requests to a server whose key matches none of them are refused:
config.pinnedCertificates = ["count.ly.cer"]While certificates are pinned, the SDK refuses a server URL that is not https.
Using a Self-Signed Server Certificate
shouldIgnoreTrustCheck accepts a server certificate the system does not trust. Use it only for testing.
config.shouldIgnoreTrustCheck = trueOther Features and Notes
SDK Config Parameters Explained
These are the properties of CountlyConfig you can set before calling start(with:), grouped by feature. Only appKey and host are mandatory.
Core
- appKey: Mandatory. The app key of your application in Countly, from Management > Applications.
-
host: Mandatory. The URL of your Countly server, starting with
https://, orhttp://when no certificate is pinned. - deviceID (default generated): A device ID of your own. Without one, the SDK generates one on the first start and keeps it.
-
features (default empty): Optional features to switch on:
.crashReportingand.pushNotifications. -
instanceName (default not set): The name of a named instance, checked against the name given to
Countly.instance(named:). Leave it unset for the default instance. - customMetrics (default empty): Device metrics to override or add, sent with the session start.
- sdkName and sdkVersion (default not set): The name and version a bridge SDK reports in place of this SDK's own.
Sessions and Events
-
manualSessionHandling (default
false): Leaves beginning, updating, and ending sessions to your application. -
enableManualSessionControlHybridMode (default
false): With manual sessions, the SDK still sends the periodic session updates. -
updateSessionPeriod (default
60,20on watchOS): Seconds between session updates and event batches. -
eventSendThreshold (default
100): The number of recorded events that sends a batch before the next session update. -
enableOrientationTracking (default
true): Records the device orientation when it changes.
Views
-
enableAutomaticViewTracking (default
false): Reports view controllers automatically on iOS, tvOS, and visionOS. - automaticViewTrackingExclusionList (default empty): View names, class names, or superclasses that automatic view tracking skips.
-
disableSwizzling (default
false): Turns automatic view tracking off for this instance. - globalViewSegmentation (default not set): Segmentation added to every view.
-
disableViewRestartForManualRecording (default
false): Keeps manual views stopped after the application returns from the background.
Device ID and Consent
-
temporaryDeviceIDMode (default
false): Starts in temporary device ID mode, which sends nothing until a device ID is set. -
resetStoredDeviceID (default
false): Drops the stored device ID at start. -
requiresConsent (default
false): Collects nothing until consent is given for a feature. - consents (default empty): The features consent is given for at start.
-
enableAllConsents (default
false): Gives consent for every feature at start.
Push Notifications
-
pushTestMode (default not set):
.developmentfor builds run from Xcode and.testFlightOrAdHocfor TestFlight and ad hoc builds. Leave it unset for App Store builds. -
sendPushTokenAlways (default
false): Sends the token even when the user has not allowed notifications. -
disableAutomaticPushHandling (default
false): Keeps the SDK out of the notification delegates, so you forward the callbacks yourself. -
doNotShowAlertForNotifications (default
false): Shows no banner for a notification that arrives while the application is in the foreground. - launchNotification (default not set): The user info of the notification that launched a macOS application.
- pushActionHandler (default not set): Sees the link of a tapped notification first and can take it over.
- pushAllowedLinkSchemes (default empty): The schemes notification links may open. Empty allows every scheme except the dangerous ones.
Location and User Profiles
- location, city, isoCountryCode, and ipAddress (default not set): The user's location, sent with the next session start.
-
disableLocation (default
false): Erases the location stored on the server and stops it from being inferred. - providedUserProperties (default not set): User properties applied before the first request leaves the device.
Remote Config
-
enableRemoteConfigAutomaticTriggers (default
false): Downloads every value at start and after a device ID change. -
enableRemoteConfigValueCaching (default
false): Keeps the values a later download did not return. -
enrollABOnRCDownload (default
false): Enrolls the user into A/B tests when values are downloaded. - remoteConfigRegisterGlobalCallback(_:): Registers a callback for every remote config download.
Crashes and Attribution
- crashSegmentation (default not set): Segmentation added to every crash report.
-
crashes.crashFilterCallback (default not set): Inspects each crash report before it is sent. Return
trueto drop it. - campaignType and campaignData (default not set): Direct attribution, sent at start when both are set.
- indirectAttribution (default not set): Advertising identifiers, such as the IDFA, sent at start.
Content
-
content.zoneTimerInterval (default
30, at least15): Seconds between content fetches. - content.globalContentCallback (default not set): Told when a content is completed or closed.
- content.contentURLHandler (default not set): Handles a link from content first.
- content.allowedLinkSchemes (default empty): The schemes links from content and feedback widgets may open. Empty allows every scheme except the dangerous ones.
-
content.webViewDisplayOption (default
.immersive,.safeAreaon macOS): Whether content covers the whole screen or stays inside the safe area. - content.overlayCornerRadius (default not set): The corner radius of the overlay. On macOS the window's radius is used when it is not set.
-
content.enableContentReloadOnStall (default
false): Loads a content again when it has not finished loading aftercontentReloadOnStallTimeoutmilliseconds (1000by default). -
content.disableZoom and content.disableRotation (default
false): Stops the user from zooming content, and keeps a content in the orientation it was shown in. -
content.showWidgetsWithinApp (default
false): On macOS, keeps content and feedback widgets inside the application window.
Networking and Security
-
requestTimeoutDuration (default
30): Seconds before a request times out. -
failedRequestCooldown (default
60): Seconds the request queue waits after a failed request. -
storedRequestsLimit (default
1000): Requests kept in the queue. Beyond it, the oldest is dropped. -
requestDropAgeHours (default
0): Drops queued requests older than this many hours.0keeps them. -
disableGradualRequestCleaner (default
false): Trims a queue overstoredRequestsLimitin one step instead of 100 requests at a time. - customNetworkRequestHeaders (default not set): HTTP headers added to every request.
-
urlSessionConfiguration (default
.default): The URL session configuration used for every request. - secretSalt (default not set): The salt for the request checksum, the same as in Countly.
- pinnedCertificates (default empty): Certificate files bundled with the application. The server's key must match one of them.
-
shouldIgnoreTrustCheck (default
false): Skips the server trust evaluation. Use it only for testing. -
disableBackoffMechanism (default
false): Keeps sending at the usual pace when the server answers slowly. -
disableWebView (default
false): Never shows content or feedback widgets.
Logging, Behavior Settings, and Diagnostics
-
enableDebug (default
false): Prints the SDK's log to the console. -
internalLogLevel (default
.debug): The lowest level that is logged. -
disableSDKLoggingInProduction (default
false): Keeps console logs off in release builds, even withenableDebugset. - loggerDelegate (default not set): Receives every log line at the configured level.
- sdkBehaviorSettings (default not set): SDK behavior settings as a JSON string, applied before the first fetch.
-
disableSDKBehaviorSettingsUpdates (default
false): Stops the SDK from fetching SDK behavior settings. -
disableHealthCheck (default
false): Turns off the health check sent at start. -
experimental.enablePreviousNameRecording and experimental.enableVisibilityTracking (default
false): Experimental segmentation, described in Experimental Features.
The limits on sdkInternalLimits are described in SDK Internal Limits.
Changing Host and App Key
A running instance can move to another server or app. What belongs to the old app is sent under the old key first:
Countly.shared.setNewHost("https://another.server.ly")
Countly.shared.setNewAppKey("ANOTHER_APP_KEY")The request queue can then be brought in line with requestQueue.replaceAllAppKeysInQueueWithCurrentAppKey() or requestQueue.removeDifferentAppKeysFromQueue().
Interacting with the Request Queue
let queue = Countly.shared.requestQueue
print(queue.count)
queue.attemptToSendStoredRequests()
queue.flushQueues()flushQueues() deletes every queued request and recorded event.
SDK Behavior Settings
SDK behavior settings let you change the SDK's behavior from Countly, such as switching tracking off or changing limits. The SDK fetches them at start and every few hours. To apply settings before the first fetch, pass them as a JSON string, and disableSDKBehaviorSettingsUpdates keeps the SDK from fetching:
config.sdkBehaviorSettings = #"{"v":2,"t":0,"c":{"tracking":true}}"#
config.disableSDKBehaviorSettingsUpdates = falseBackoff Mechanism
When the server answers slowly, the SDK waits before sending more, so a busy server is not flooded. disableBackoffMechanism turns this off.
Health Checks
The SDK sends one health check per launch with counts of its own errors and warnings, which show in Countly under Management > SDK Manager. disableHealthCheck turns it off.
Attribution
Report the campaign a user came from:
Countly.shared.attribution.recordDirectAttribution(campaignType: "countly", campaignData: #"{"cid":"CAMPAIGN_ID","cuid":"USER_ID"}"#)
Countly.shared.attribution.recordIndirectAttribution(["idfa": "IDFA_VALUE"])Both can be given at start with campaignType and campaignData, and indirectAttribution.
Custom Metrics
Override or add device metrics sent with the session start:
config.customMetrics = ["_app_version": "2.1.0-beta", "store": "enterprise"]Countly.shared.recordMetrics(["_carrier": "Example Telecom"])Custom HTTP Header Values
config.customNetworkRequestHeaders = ["X-Tenant": "acme"]Countly.shared.addCustomNetworkRequestHeaders(["X-Tenant": "globex"])Every request is an HTTP POST. urlSessionConfiguration lets you supply a URL session configuration of your own.
Direct Requests
Countly.shared.addDirectRequest(["custom_param": "value"])Keys the SDK sets itself, such as the device ID and the app version, are dropped.
SDK Internal Limits
The SDK limits the data you record, so that a mistake cannot send large amounts of it to your server. A value over its limit is truncated, and where a count is limited the oldest items are dropped. Set the limits on sdkInternalLimits before start. SDK behavior settings can change them later. You can check the exact parameters these limits affect here.
Key Length
Limits the length of every key you set, such as event keys, view names, segmentation keys, and user property keys (default: 128 characters):
config.sdkInternalLimits.maxKeyLength = 64Value Size
Limits the length of string values, such as segmentation values and user property values (default: 256 characters). A profile picture URL has its own limit, maxValueSizePicture (default: 4096 characters):
config.sdkInternalLimits.maxValueSize = 128Segmentation Values
Limits the number of key-value pairs in one segmentation (default: 100):
config.sdkInternalLimits.maxSegmentationValues = 50Breadcrumb Count
Limits the number of crash breadcrumbs kept. Recording one more drops the oldest (default: 100):
config.sdkInternalLimits.maxBreadcrumbCount = 50Stack Trace Lines Per Thread
Limits the number of stack trace lines recorded per thread (default: 30):
config.sdkInternalLimits.maxStackTraceLinesPerThread = 20Stack Trace Line Length
Limits the characters of each stack trace line and of the crash message (default: 200):
config.sdkInternalLimits.maxStackTraceLineLength = 150Suspending and Halting
Countly.shared.suspend()
Countly.shared.resume()
Countly.shared.halt(clearStorage: false)suspend() sends what is pending and closes the session, and resume() continues tracking. halt(clearStorage:) stops the instance; with clearStorage: true it erases everything it stored.
Experimental Features
experimental.enablePreviousNameRecording adds the previous view's name to views and the current view's name and the previous event's key to events. experimental.enableVisibilityTracking records whether the application was visible when an event was recorded.
config.experimental.enablePreviousNameRecording = true
config.experimental.enableVisibilityTracking = true