This document includes necessary information for integrating the Countly iOS SDK into in your iOS / watchOS / tvOS / macOS applications, and applies to version 23.12.X
.
Click here, to access the documentation for older SDK versions.
The Countly iOS SDK supports minimum Deployment Target
iOS 10.0 (watchOS 4.0, tvOS 10.0, macOS 10.14), and it requires Xcode 13.0+.
Adding the SDK to the project
To add the Countly iOS SDK into your project, you can choose one of the following options:
- Download the Countly iOS SDK source files directly from GitHub, add all .h
and .m
files in the countly-ios-sdk
folder of your project on Xcode.
- Clone the Countly iOS SDK repo as a Git submodule.
- Using Swift Package Manager (SPM)
- Using Carthage
- Using CocoaPods
SDK Integration
Minimal Setup
In your application delegate, import Countly.h
, and add the following lines at the beginning insideapplication:didFinishLaunchingWithOptions:
#import "Countly.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CountlyConfig* config = CountlyConfig.new;
config.appKey = @"YOUR_APP_KEY";
config.host = @"https://YOUR_COUNTLY_SERVER";
[Countly.sharedInstance startWithConfig:config];
// your code
return YES;
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
let config: CountlyConfig = CountlyConfig()
config.appKey = "YOUR_APP_KEY"
config.host = "https://YOUR_COUNTLY_SERVER"
Countly.sharedInstance().start(with: config)
// your code
return true
}
Note: Make sure you start Countly iOS SDK on the main thread.
Set your app key and host on the CountlyConfig
object. Please check here for more information about acquiring application key (APP_KEY) and server URL.
If you are using the Countly Enterprise Edition trial servers, the host
should be https://try.count.ly
, https://us-try.count.ly
or https://asia-try.count.ly
. Stated simply, it should be the domain from which you are accessing your trial dashboard.
You can run your project and see the first session data immediately displayed on your Countly Server dashboard.
If you are in doubt about the correctness of your Countly SDK integration, you can learn about the verification methods from here.
Additional Features
If you would like to use additional features, such as PushNotifications, CrashReporting, and AutoViewTracking, you can specify them in the features
array on the CountlyConfig
object before you start:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CountlyConfig* config = CountlyConfig.new;
config.appKey = @"YOUR_APP_KEY";
config.host = @"https://YOUR_COUNTLY_SERVER";
//You can specify additional features you want here
config.features = @[CLYPushNotifications, CLYCrashReporting, CLYAutoViewTracking];
[Countly.sharedInstance startWithConfig:config];
// your code
return YES;
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
let config: CountlyConfig = CountlyConfig()
config.appKey = "YOUR_APP_KEY"
config.host = "https://YOUR_COUNTLY_SERVER"
//You can specify additional features you want here
config.features = [CLYPushNotifications, CLYCrashReporting, CLYAutoViewTracking]
Countly.sharedInstance().start(with: config)
// your code
return true
}
Available additional features per platform:
iOS
CLYPushNotifications
CLYCrashReporting
CLYAutoViewTracking
watchOS
CLYCrashReporting
tvOS
CLYCrashReporting
CLYAutoViewTracking
macOS
CLYPushNotifications
CLYCrashReporting
Debug Mode
If you would like to enable the Countly iOS SDK to debug mode, which logs internal info, errors, and warnings into your console, you can set the enableDebug
flag on the CountlyConfig
object before starting Countly.
config.enableDebug = YES;
config.enableDebug = true
Internal logging works only for Development environment where DEBUG
flag is set in target's Build settings. But if you still can not see the SDK logs you have to make sure your XCode is configured correctly by going to Product > Scheme > Edit Scheme in your XCode and selecting Run from the menu at the left-hand side. There you should click on Arguments tab and make sure "OS_ACTIVITY_MODE" argument is set to "disable".
For more information on where to find the SDK logs you can check the documentation here.
Logger Delegate
For receiving the Countly iOS SDK's internal logs even in production builds, you can set loggerDelegate
property on the CountlyConfig
object. If set, the Countly iOS SDK will forward its internal logs to this delegate object regardless of enableDebug
initial config value.
config.loggerDelegate = self; //or any other object to act as CountlyLoggerDelegate
config.loggerDelegate = self //or any other object to act as CountlyLoggerDelegate
internalLog:
method declared as required
in CountlyLoggerDelegate
protocol will be called with log NSString
.
// CountlyLoggerDelegate protocol method
- (void)internalLog:(NSString *)log
{
}
// CountlyLoggerDelegate protocol method
func internalLog(_ log: String)
{
}
SDK Data Storage
The Countly iOS SDK uses NSUserDefaults
and a simple data file named Countly.dat
under NSApplicationSupportDirectory
(NSCachesDirectory
for tvOS).
The Countly Code Generator can be used to generate Countly iOS SDK code snippets effortlesly. You can provide values for your events, user profiles, or just start with basic integration. It will generate the necessary code for you.
Crash Reporting
Automatic Crash Handling
For Countly Crash Reporting, you'll need to specify CLYCrashReporting
in features
array on the CountlyConfig
object before starting Countly.
config.features = @[CLYCrashReporting];
config.features = [CLYCrashReporting]
With this feature, the Countly iOS SDK will generate a crash report if your application crashes due to an exception and send it to the Countly Server for further inspection. If a crash report cannot be delivered to the server (e.g. no internet connection, unavailable server) at the time of the crash, the Countly iOS SDK will then store the crash report locally in order to make another attempt at a later time.
Handled Exceptions
The SDK provides functionality to manually report exceptions:
NSException* myException = [NSException exceptionWithName:@"MyException" reason:@"MyReason" userInfo:@{@"key":@"value"}];
[Countly.sharedInstance recordException:myException];
let myException : NSException = NSException.init(name:NSExceptionName(rawValue: "MyException"), reason:"MyReason", userInfo:["key":"value"])
Countly.sharedInstance().recordException(myException)
By default, the reported exception will be marked as "fatal". Though you may want to override it and record a non fatal exception:
NSException* myException = [NSException exceptionWithName:@"MyException" reason:@"MyReason" userInfo:@{@"key":@"value"}];
[Countly.sharedInstance recordException:myException isFatal:NO];
let myException : NSException = NSException.init(name:NSExceptionName(rawValue: "MyException"), reason:"MyReason", userInfo:["key":"value"])
Countly.sharedInstance().recordException(myException, isFatal: false)
There is also an extended call where you can pass the fatality information, stack trace and segmentation:
NSException* myException = [NSException exceptionWithName:@"MyException" reason:@"MyReason" userInfo:@{@"key":@"value"}];
NSDictionary* segmentation = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance recordException:myException isFatal:YES stackTrace:[NSThread callStackSymbols] segmentation:segmentation];
let myException : NSException = NSException.init(name:NSExceptionName(rawValue: "MyException"), reason:"MyReason", userInfo:["key":"value"])
let segmentation : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().recordException(myException, isFatal: true, stackTrace: Thread.callStackSymbols, segmentation:segmentation)
Record Swift Error
The SDK offers a call to record swift errors:
[Countly.sharedInstance recordError:@"ERROR_NAME" stackTrace:[NSThread callStackSymbols]];
Countly.sharedInstance().recordError("ERROR_NAME", stackTrace: Thread.callStackSymbols)
There is also an extended version where you can pass fatality information, stack trace and segmentation:
NSDictionary* segmentation = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance recordError:@"ERROR_NAME" isFatal:YES stackTrace:[NSThread callStackSymbols] segmentation:segmentation];
let myException : NSException = NSException.init(name:NSExceptionName(rawValue: "MyException"), reason:"MyReason", userInfo:["key":"value"])
let segmentation : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().recordError("ERROR_NAME", isFatal: true, stackTrace: Thread.callStackSymbols, segmentation:segmentation)
Crash Breadcrumbs
You can use the recordCrashLog:
method to receive custom logs with the crash reports. Logs generated by the recordCrashLog:
method are stored in a non-persistent structure and are delivered to the Countly Server only for a crash.
[Countly.sharedInstance recordCrashLog:@"This is a custom crash log."];
Countly.sharedInstance().recordCrashLog("This is a custom crash log.")
There is a limit for the number of crash logs to be stored on the device. By default it is 100. You can change it using crashLogLimit
property on the CountlyConfig
object.
config.crashLogLimit = 500;
config.crashLogLimit = 500
If number of stored crash logs reaches the limit, SDK will start to drop oldest crash log while appending the newest one.
Crash Report Contents
A crash report includes the following information:
Default Crash Report Information
- Exception Info:
* Exception Name
* Exception Description
* Stack Trace
* Binary Images
- Device Static Info:
* Device Type
* Device Architecture
* Resolution
* Total RAM
* Total Disk
- Device Dynamic Info:
* Used RAM
* Used Disk
* Battery Level
* Connection Type
* Device Orientation
- OS Info:
* OS Name
* OS Version
* OpenGL ES Version
* Jailbrake State
- App Info:
* App Version
* App Build Number
* Executable Name
* Time Since App Launch
* Background State
- Custom Info:
* Crash logs recorded using `recordCrashLog:` method
* Crash segmentation specified in `crashSegmentation` property
Custom Crash Segmentation
If you would like to use custom crash segmentation, you can set the optional crashSegmentation
dictionary on the CountlyConfig
object.
config.crashSegmentation = @{@"key":@"value"};
config.crashSegmentation = ["key":"value"]
Crash Filtering
You can set the optional crashFilter
regular expression on the CountlyConfig
object for filtering crash reports and preventing them from being sent to Countly Server. If a crash's name, description or any line of stack trace matches the given regular expression, it will not be sent to Countly Server.
config.crashFilter = [NSRegularExpression regularExpressionWithPattern:@".*keyword.*" options:NSRegularExpressionCaseInsensitive error:nil];
config.crashFilter = try! NSRegularExpression(pattern: ".*keyword.*", options: NSRegularExpression.Options.caseInsensitive)
PLCrashReporter
As an alternative to Countly iOS SDK's own exception and signal handling mechanism based on NSSetUncaughtExceptionHandler()
and signal()
functions, you can optionally use the more advanced PLCrashReporter as well.
For using PLCrashReporter instead of default crash handling mechanism you can set shouldUsePLCrashReporter
flag on the CountlyConfig
object.
If set, Countly iOS SDK will be using PLCrashReporter dependency for creating crash reports.
config.shouldUsePLCrashReporter = YES;
config.shouldUsePLCrashReporter = true
And PLCrashReporter framework dependency should be added to your project manually from its GitHub releases page or via CocoaPods using Countly-PL.podspec
instead of default Countly.podspec
.
Existence of PLCrashReporter dependency will be checked using __has_include(<CrashReporter/CrashReporter.h>)
preprocessor macro.
Note: PLCrashReporter option is available only for iOS apps.
Note: Currently, tested and supported PLCrashReporter version is 1.5.1.
PLCrashReporter Signal Handler Type
PLCrashReporter has two different signal handling implementations with different traits:
1) BSD: PLCrashReporterSignalHandlerTypeBSD
2) Mach: PLCrashReporterSignalHandlerTypeMach
For more information about PLCrashReporter please see: https://github.com/microsoft/plcrashreporter
By default, BSD type will be used. For using Mach type signal handler with PLCrashReporter you can set shouldUseMachSignalHandler
flag on the CountlyConfig
object.
config.shouldUseMachSignalHandler = YES;
config.shouldUseMachSignalHandler = true
PLCrashReporter Callback Blocks
There is a crashOccuredOnPreviousSessionCallback
block to be executed when the app is launched again following a crash which is detected by PLCrashReporter on the previous session. It has an NSDictionary
parameter that represents crash report object. If shouldUsePLCrashReporter
flag is not set on initial config, this block will never be executed. You can set it on the CountlyConfig
object:
config.crashOccuredOnPreviousSessionCallback = ^(NSDictionary * crashReport)
{
NSLog(@"crash report: %@", crashReport);
};
config.crashOccuredOnPreviousSessionCallback =
{
(crashReport: [AnyHashable: Any]) in print("crash report: \(crashReport)")
}
There is also another shouldSendCrashReportCallback
block to be executed to decide whether the crash report detected by PLCrashReporter on the previous session should be sent to Countly Server or not. If not set, crash report will be sent to Countly Server by default. If set, crash report will be sent to Countly Server only if YES
is returned. It has an NSDictionary
parameter that represents crash report object. If shouldUsePLCrashReporter
flag is not set on initial config, this block will never be executed. You can set it on the CountlyConfig
object:
config.shouldSendCrashReportCallback = ^(NSDictionary * crashReport)
{ NSLog(@"crash report: %@", crashReport);
return YES; //NO;
};
config.shouldSendCrashReportCallback =
{
(crashReport: [AnyHashable: Any]) in print("crash report: \(crashReport)")
return true //false
}
Symbolication
This feature is only available with an Enterprise Edition subscription.
Symbolication is the process of converting stack trace memory addresses in crash reports into human-readable, useful information, such as class/method names, file names, and line numbers.
In order to symbolicate memory addresses, the dSYM files for each build need to be uploaded to the Countly Server.
Automatic dSYM Uploading
For Automatic dSYM Uploading, you can use the countly_dsym_uploader
script in the Countly iOS SDK.
To do so, go to the Build Phases
section of your app target and click on the plus ( + ) icon on the top left, then choose New Run Script Phase
from the list.
Then, add the following snippet:
COUNTLY_DSYM_UPLOADER=$(/usr/bin/find $SRCROOT -name "countly_dsym_uploader.sh" | head -n 1)
sh "$COUNTLY_DSYM_UPLOADER" "https://YOUR_COUNTLY_SERVER" "YOUR_APP_KEY"
Next, select the checkboxRun script only when installing
.
Note: Do not forget to replace your server and app key.
By default, Xcode will generate dSYM files for the Release build configuration, and the countly_dsym_uploader
script will handle the uploading automatically. You can check for the results on the Report Navigator within Xcode. If the dSYM upload has completed successfully, you will see the[Countly] dSYM upload successfully completed.
message.
If there are any errors while uploading the dSYM file, you can see these error messages in the Report Navigator. Some of the possible error reasons include: the dSYM file not being created due to build configurations, the dSYM file being created at a non-default location, wrong App ID and/or Countly Server address, or network unavailability.
Manual dSYM Uploading
In case of an error with Automatic dSYM Uploading, or if you would like to upload your dSYM files manually, you can use our guide for Manual dSYM Uploading here. You will also need to use Manual dSYM Uploading if Bitcode is enabled while uploading your app to App Store Connect.
Bitcode Enabled Apps
If Bitcode is enabled in your project while uploading your app to App Store Connect, Apple re-compiles your app to optimize it for specific devices. When Apple re-compiles your app, a new dSYM file is generated for the new build, and the dSYM file on your machine will not work for symbolication. So, you will need to receive this new dSYM file manually, then upload it to the Countly Server. In order to get the new dSYM file, you can use App Store Connect or Xcode Organizer.
Using App Store Connect: 1. Login to App Store Connect
. 2. Go to the Activity
tab. 3. Select your app's Version
and Build
4. Under General Information
click on Download dSYM
. 5. If the downloaded file does not have any extension, add .zip
and unarchive to see its content.
Using Xcode: 1. Open Organizer
in Xcode. 2. Go to the Archives
tab. 3. Select your app from the list on the left and select the archive. 4. Click on Download dSYMs...
. 5. Xcode inserts the downloaded .dSYM files into the selected archive.
For more information regarding downloading dSYM files from Apple, please see Apple's documentation here.
After you receive your dSYM file from Apple, you can use our Manual dSYM Uploading guide.
How to Use Symbolication
Once your dSYM file has been uploaded to the Countly Server, you can symbolicate your crash reports coming from that build on the Crashes
panel of your Countly Server.
A crash report symbolicated stack trace appears as follows:
Before symbolication:
YourAppName 0x000000010006e174 YourAppName + 156020
YourAppName 0x000000010006d060 YourAppName + 151648
YourAppName 0x000000010006ad34 YourAppName + 142644
After symbolication:
-[MHViewController countlyProductionTest] (in YourAppName) (MHViewController.m:620)
-[MHViewController transitionToMahya] (in YourAppName) (MHViewController.m:443)
-[MHViewController textFieldShouldReturn:] (in YourAppName) (MHViewController.m:210)
For more information about how to use the Symbolication feature on the Countly Server, please see our Symbolication documentation here.
Events
Here is a quick summary on how to use event recording methods:
Recording Events
We have recorded an event named purchase with different scenarios in the examples below:
- purchase event occurred 1 time
[Countly.sharedInstance recordEvent:@"purchase"];
Countly.sharedInstance().recordEvent("purchase")
- purchase event occurred 3 times
[Countly.sharedInstance recordEvent:@"purchase" count:3];
Countly.sharedInstance().recordEvent("purchase", count:3)
- purchase event occurred 1 times with the total amount of 3.33
[Countly.sharedInstance recordEvent:@"purchase" sum:3.33];
Countly.sharedInstance().recordEvent("purchase", sum:3.33)
- purchase event occurred 3 times with the total amount of 9.99
[Countly.sharedInstance recordEvent:@"purchase" count:3 sum:9.99];
Countly.sharedInstance().recordEvent("purchase", count:3, sum:3.33)
- purchase event occurred 1 time from country : Germany, on app_version : 1.0
NSDictionary* dict = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance recordEvent:@"purchase" segmentation:dict];
let dict : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().recordEvent("purchase", segmentation:dict)
- purchase event occurred 2 times from country : Germany, on app_version : 1.0
NSDictionary* dict = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance recordEvent:@"purchase" segmentation:dict count:2];
let dict : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().recordEvent("purchase", segmentation:dict, count:2)
- purchase event occurred 2 times with the total amount of 6.66, from country: Germany, on app_version : 1.0
NSDictionary* dict = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance recordEvent:@"purchase" segmentation:dict count:2 sum:6.66];
let dict : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().recordEvent("purchase", segmentation:dict, count:2, sum:6.66)
Timed Events
In the examples below, we recorded a timed event called level24 to track how long it takes to complete:
- level24 started
[Countly.sharedInstance startEvent:@"level24"];
Countly.sharedInstance().startEvent("level24")
- level24 ended
[Countly.sharedInstance endEvent:@"level24"];
Countly.sharedInstance().endEvent("level24")
Additionally, you can provide more information, such as the segmentation, count, and sum while ending an event.
- level24 ended 1 time with the total point of 34578, from country : Germany, on app_version : 1.0
NSDictionary* dict = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance endEvent:@"level24" segmentation:dict count:1 sum:34578];
let dict : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().endEvent("level24", segmentation:dict, count:1, sum:34578)
The duration of the event will be calculated automatically when the endEvent
method is called.
You can also cancel a started timed event using cancelEvent
method:
[Countly.sharedInstance cancelEvent:@"level24"];
Countly.sharedInstance().cancelEvent("level24")
Or, if you are measuring the duration of an event yourself, you can record it directly as follows:
- level24 took 344 seconds to complete:
[Countly.sharedInstance recordEvent:@"level24" duration:344];
Countly.sharedInstance().recordEvent("level24", duration:344)
Additionally, you can provide more information such as the segmentation, count, and sum.
- level24 took 344 seconds to complete 2 times with the total point of 34578, from country : Germany, on app_version : 1.0
NSDictionary* dict = @{@"country":@"Germany", @"app_version":@"1.0"};
[Countly.sharedInstance recordEvent:@"level24" segmentation:dict count:2 sum:34578 duration:344];
let dict : Dictionary<String, String> = ["country":"Germany", "app_version":"1.0"]
Countly.sharedInstance().recordEvent("level24", segmentation:dict, count:2, sum:34578, duration:344)
Event names must be non-zero length valid NSString
and segmentation must be an NSDictionary
which does not contain any custom objects, as it will be converted to JSON.
Sessions
Automatic Session Tracking
By default, the Countly iOS SDK tracks sessions automatically and sends the begin_session
request upon initialization, the end_session
request when the app goes to the background, and the begin_session
request again when the app comes back to the foreground. In addition, the Countly iOS SDK automatically sends a periodical (60 sec by default) update session request while the app is in the foreground.
Manual Sessions
You can set the manualSessionHandling
flag on the CountlyConfig
object before starting Countly to handle sessions manually.
config.manualSessionHandling = YES;
config.manualSessionHandling = true
If the manualSessionHandling
flag is set, the Countly iOS SDK does not send the previously mentioned requests automatically, meaning you will need to manually call the beginSession
, updateSession
and endSession
methods after you start Countly, depending on your own definition of a session.
[Countly.sharedInstance beginSession];
[Countly.sharedInstance updateSession];
[Countly.sharedInstance endSession];
Countly.sharedInstance().beginSession()
Countly.sharedInstance().updateSession()
Countly.sharedInstance().endSession()
Update Session Period
You can specify the updateSessionPeriod
on the CountlyConfig
object before starting Countly. It is used for session updating and periodically sending queued events to the server. If the updateSessionPeriod
is not explicitly set, the default setting will be at 60 seconds for iOS, tvOS & macOS, and 20 seconds for watchOS.
config.updateSessionPeriod = 300;
config.updateSessionPeriod = 300
View Tracking
Automatic Views
To enable automatic view tracking, you will need to set the enableAutomaticViewTracking
flag on the CountlyConfig
object before starting Countly.
config.enableAutomaticViewTracking = YES;
config.enableAutomaticViewTracking = true
After this step, the Countly iOS SDK will automatically track views by simply intercepting the viewDidAppear:
method of the UIViewController
class and reporting which view is displayed with the view name and duration. If the view controller's title
property is set, it would be reported as the view name. Otherwise, the view name will be the view controller's class name.
Automatic View Exceptions
Default Exceptions for Automatic View Tracking
Following system view controllers will be excluded by default from automatic view tracking, as they are not visible to the user but rather structural controllers:
UINavigationController
UIAlertController
UIPageViewController
UITabBarController
UIReferenceLibraryViewController
UISplitViewController
UIInputViewController
UISearchController
UISearchContainerViewController
UIApplicationRotationFollowingController
MFMailComposeInternalViewController
MFMailComposeInternalViewController
MFMailComposePlaceholderViewController
UIInputWindowController
_UIFallbackPresentationViewController
UIActivityViewController
UIActivityGroupViewController
_UIActivityGroupListViewController
_UIActivityViewControllerContentController
UIKeyboardCandidateRowViewController
UIKeyboardCandidateGridCollectionViewController
UIPrintMoreOptionsTableViewController
UIPrintPanelTableViewController
UIPrintPanelViewController
UIPrintPaperViewController
UIPrintPreviewViewController
UIPrintRangeViewController
UIDocumentMenuViewController
UIDocumentPickerViewController
UIDocumentPickerExtensionViewController
UIInterfaceActionGroupViewController
UISystemInputViewController
UIRecentsInputViewController
UICompatibilityInputViewController
UIInputViewAnimationControllerViewController
UISnapshotModalViewController
UIMultiColumnViewController
UIKeyCommandDiscoverabilityHUDViewController
Custom Exceptions for Automatic View Tracking
In addition to these default exceptions, you can manually set an exclusion list of the view controllers you don't want to track by using the automaticViewTrackingExclusionList
array on the CountlyConfig
object before starting Countly
config.automaticViewTrackingExclusionList = @[NSStringFromClass(MyViewController.class), @"MyViewControllerName"];
config.automaticViewTrackingExclusionList = [NSStringFromClass(MyViewController.class), "MyViewControllerName"];
View controller class names or titles from this list will be ignored by automatic view tracking and their appearances will not be reported. Adding an already excluded view controller class name or title a second time will have no effect.
Customizing Auto View Tracking View Names
You can utilize CountlyAutoViewTrackingName
protocol to customize view names used by Auto View Tracking.
//Make your view controller to conform CountlyAutoViewTrackingName protocol.
@interface MyViewController : UIViewController @end
//and implement countlyAutoViewTrackingName method to return custom view name to be used by Auto View Tracking.
- (NSString *)countlyAutoViewTrackingName { return @"This is overridden custom view name"; }
//Make your view controller to conform CountlyAutoViewTrackingName protocol.
class MyViewController: UIViewController, CountlyAutoViewTrackingName
//and implement countlyAutoViewTrackingName function to return custom view name to be used by Auto View Tracking.
func countlyAutoViewTrackingName() -> String { return "This is overridden custom view name" }
Manual View Recording
Please be aware that if auto view tracking is enabled, manual view tracking will not be taken into account.
If you want to have full control over your view tracking you can use manual view recording methods.
Auto Stopped Views
A view initiated with auto stopped view method is designed to be automatically stopped when this method is called again. You should use startAutoStoppedView:
method with a view name. This method begins tracking a view and returns a unique identifier.
[Countly.sharedInstance.views startAutoStoppedView:@"MyView"];
Countly.sharedInstance().views.startAutoStoppedView("MyView")
You can also specify the custom segmentation key-value pairs while starting views:
[Countly.sharedInstance.views startAutoStoppedView:@"MyView" segmentation:@{@"key": @"value"}];
Countly.sharedInstance().views.startAutoStoppedView("MyView", segmentation: ["key": "value"])
Regular Views
Opposed to "auto stopped views", with regular views you can have multiple of them started at the same time, and then you can control them independently. You can manually start a view using the startView:
method with a view name. This will start tracking a view and return a unique identifier, and the view will remain active until explicitly stopped using stopViewWithName:
or stopViewWithID:
[Countly.sharedInstance.views startView:@"MyView"];
Countly.sharedInstance().views.startView("MyView")
You can also specify the custom segmentation key-value pairs while starting views:
[Countly.sharedInstance.views startView:@"MyView" segmentation:@{@"key": @"value"}];
Countly.sharedInstance().views.startView("MyView", segmentation: ["key": "value"])
Stopping Views
If there are multiple views with the same name (they would have different identifiers) but if you try to stop one with that name the SDK would close one of those randomly.
You can stop view tracking by its name using stopViewWithName:
[Countly.sharedInstance.views stopViewWithName:@"MyView"];
Countly.sharedInstance().views.stopViewWithName("MyView")
This function allows you to manually stop the tracking of a view identified by its name.
You can also specify the custom segmentation key-value pairs while stopping views:
[Countly.sharedInstance.views stopViewWithName:@"MyView" segmentation:@{@"key": @"value"}];
Countly.sharedInstance().views.stopViewWithName("MyView", segmentation: ["key": "value"])
You can also stop view tracking by its unique idetifier using stopViewWithID:
[Countly.sharedInstance.views stopViewWithID:@"VIEW_ID"];
Countly.sharedInstance().views.stopViewWithID("VIEW_ID")
This function allows you to manually stop the tracking of a view identified by its unique identifier.
You can also specify the custom segmentation key-value pairs while stopping views:
[Countly.sharedInstance.views stopViewWithID:@"VIEW_ID" segmentation:@{@"key": @"value"}];
Countly.sharedInstance().views.stopViewWithID("VIEW_ID", segmentation: ["key": "value"])
You can stop all views tracking using stopAllViews:
[Countly.sharedInstance.views stopAllViews:@{@"key": @"value"}];
Countly.sharedInstance().views.stopAllViews(["key": "value"])
This function stops the tracking of all views.
Pausing and Resuming Views
The iOS SDK allows you to start multiple views at the same time. If you are starting multiple views at the same time it might be necessary for you to pause some views while others are still continuing. This can be achieved by using the unique identifier you get while starting a view.
You can pause view tracking by its unique identifier using pauseViewWithID:
[Countly.sharedInstance.views pauseViewWithID:@"VIEW_ID"];
Countly.sharedInstance().views.pauseViewWithID("VIEW_ID")
This function temporarily pauses the tracking of a view identified by its unique identifier.
You can resume view tracking by its unique identifier using resumeViewWithID:
[Countly.sharedInstance.views resumeViewWithID:@"VIEW_ID"];
Countly.sharedInstance().views.resumeViewWithID("VIEW_ID")
This function resumes the tracking of a previously paused view identified by its unique identifier.
Adding Segmentation to Started Views
You can also add segmentation to already started views using view name or view ID:
[Countly.sharedInstance.views addSegmentationToViewWithID:@"VIEW_ID" segmentation:@{@"key": @"value"}];
[Countly.sharedInstance.views addSegmentationToViewWithName:@"VIEW_NAME" segmentation:@{@"key": @"value"}];
Countly.sharedInstance().views.addSegmentationToViewWithID("VIEW_ID", segmentation["key": "value"])
Countly.sharedInstance().views.addSegmentationToViewWithName("VIEW_NAME", segmentation["key": "value"])
Global View Segmentation
You can set global segmentation for views by using setGlobalViewSegmentation:
[Countly.sharedInstance.views setGlobalViewSegmentation:@{@"key": @"value"}"];
Countly.sharedInstance().views.setGlobalViewSegmentation(["key": "value"])
You can also update global segmentation values for views by using updateGlobalViewSegmentation:
[Countly.sharedInstance.views updateGlobalViewSegmentation:@{@"key": @"value"}];
Countly.sharedInstance().views.updateGlobalViewSegmentation(["key": "value"])
Device ID Management
On iOS, iPadOS, and tvOS, the default device ID is the Identifier For Vendor (IDFV). On watchOS and macOS, it is a persistently stored random NSUUID
string.
If you would like to use a custom device ID, you can set the deviceID
property on the CountlyConfig
object. If the deviceID
property is not set explicitly, a default device ID will be used depending on the platform.
config.deviceID = @"customDeviceID"; //Optional custom device ID
config.deviceID = "customDeviceID" //Optional custom device ID
Note: Once set, the device ID will be persistently stored on the device after the first app launch, and the deviceID
property will be ignored on the following app launches, until the app is deleted and re-installed or a resetStoredDeviceID
flag is set. For further details, please check the Resetting Stored Device ID section below.
Changing Device ID
You can use the changeDeviceIDWithMerge:
or changeDeviceIDWithoutMerge:
method to change the device ID on runtime after you start Countly. You can either allow the device to be counted as a new device or merge existing data on the server.
With this method changeDeviceIDWithMerge:
the old device ID on the server will be replaced with the new one, and data associated with the old device ID will be merged automatically.
With changeDeviceIDWithoutMerge:
a new device ID created on the server.
//change and merge on server
[Countly.sharedInstance changeDeviceIDWithMerge:@"new_device_id"];
//no replace and merge on server, device will be counted as new
[Countly.sharedInstance changeDeviceIDWithoutMerge:@"new_device_id"];
//replace and merge on server
Countly.sharedInstance().changeDeviceIDWithMerge("new_device_id")
//no replace and merge on server, device will be counted as new
Countly.sharedInstance().changeDeviceIDWithoutMerge("new_device_id")
If device ID is changed without merging and requiresConsent
flag was enabled, all previously given consents will be removed. This means that all features will cease to function until new consent has been given again for the new device ID.
Temporary Device ID
You can use temporary device ID mode for keeping all requests on hold until the real device ID is set later. It can be enabled by setting deviceID
on initial configuration asCLYTemporaryDeviceID
:
config.deviceID = CLYTemporaryDeviceID;
config.deviceID = CLYTemporaryDeviceID
Or by passing as an argument for deviceID
parameter on changeDeviceIDWithoutMerge:
any time:
[Countly.sharedInstance changeDeviceIDWithoutMerge:CLYTemporaryDeviceID];
Countly.sharedInstance().changeDeviceIDWithoutMerge(CLYTemporaryDeviceID)
As long as the device ID value is CLYTemporaryDeviceID
, the SDK will be in temporary device ID mode and all requests will be on hold, but they will be persistently stored.
When in temporary device ID mode, method calls for presenting feedback widgets and updating remote config will be ignored.
Later, when the real device ID is set using changeDeviceIDWithMerge:
or changeDeviceIDWithoutMerge:
method, all requests which have been kept on hold until that point will start with the real device ID:
[Countly.sharedInstance changeDeviceIDWithMerge:@"new_device_id"];
[Countly.sharedInstance changeDeviceIDWithoutMerge:@"new_device_id"];
Countly.sharedInstance().changeDeviceIDWithMerge("new_device_id")
Countly.sharedInstance().changeDeviceIDWithoutMerge("new_device_id")
Note: When setting real device ID while the current device ID is CLYTemporaryDeviceID
, with merge or without merge does not matter.
If the SDK goes into Temporary Device ID mode and requiresConsent
flag was enabled, all previously given consents will be removed. Therefore after entering the Temporary Device ID mode, you should reestablish consent again.
Retrieving Current Device ID
You can use deviceID
method to get current device ID:
[Countly.sharedInstance deviceID];
Countly.sharedInstance().deviceID()
It can be used for handling data export and/or removal requests as part of your app's data privacy compliance.
You can use deviceIDType
method which returns a CLYDeviceIDType
to get current device ID type:
[Countly.sharedInstance deviceIDType];
Countly.sharedInstance().deviceIDType()
Device ID type can be one of the following:CLYDeviceIDTypeCustom
: Custom device ID set by app developer.CLYDeviceIDTypeTemporary
: Temporary device ID.CLYDeviceIDTypeIDFV
: Default device ID type used by the SDK on iOS and tvOS.CLYDeviceIDTypeNSUUID
: Default device ID type used by the SDK on watchOS and macOS.
Resetting Stored Device ID
In order to handle device ID changes for logged-in and logged-out users, the device ID specified in the CountlyConfig
object of the deviceID
property (or the default device ID, if not specified) will be persistently stored as well as the device ID passed to the changeDeviceIDWithMerge:
or changeDeviceIDWithoutMerge:
method at any time upon the first app launch. By this point, until you delete and re-install the app, the Countly iOS SDK will continue to use the stored device ID and ignore the deviceID
property. So, if you set the deviceID
property to something different upon future app launches during development, it will have no effect. In this case, you can set the resetStoredDeviceID
flag on the CountlyConfig
object in order to reset the stored device ID. This will reset the initially stored device ID and the Countly iOS SDK will work as if it is the first app launch.
config.resetStoredDeviceID = YES;
config.resetStoredDeviceID = true
After you start Countly once with the resetStoredDeviceID
flag while developing, you can remove that line. The resetStoredDeviceID
flag is not meant for production. It is only for debugging purposes while performing development and not being able to delete and re-install the app.
Push Notifications
To disable push notifications in your app and avoid App Store Connect warnings, you can define the macro "COUNTLY_EXCLUDE_PUSHNOTIFICATIONS" in your project's preprocessor macros setting. The location of this setting will vary depending on the development environment you are using.
For example, in Xcode, you can define this macro by navigating to the project settings, selecting the build target, and then selecting the "Build Settings" tab. Under the "Apple LLVM - Preprocessing" section, you will find the "Preprocessor Macros" where you can add the macro "COUNTLY_EXCLUDE_PUSHNOTIFICATIONS" to the Debug and/or Release fields. This will exclude push notifications from the build and avoid the App Store Connect warnings.
Setting up APNs Authentication
First, you will need to acquire Push Notification credentials from Apple using one of the following methods:
A) APNs Auth Key (preferred)
B) Universal (Sandbox + Production) Certificate
A) Getting an APNs Auth Key
APNs Auth Key is the preferred authentication method on APNs for a number of reasons, including less issues faced during configuration and the fact that it can reuse the same connection with multiple apps.
First go to the Create a New Key section on the Apple Developer website to get an APNs Auth Key.
Check the APNs
option and create your key.
Then download your key and store it in a safe place, you won't be able to download it again.
You'll also need some identifiers to upload a key file to Countly:
-
Key ID
(filled automatically if you kept the original Auth Key filename, otherwise visible on the key details panel) -
Team ID
(see Membership section) -
Bundle ID
(see App IDs section)
B) Getting APNs Universal (Sandbox + Production) Certificate
Please go to the Certificates section on the Certificates, Identifiers & Profiles page on the Apple Developer website. Click the plus sign and select the Apple Push Notification service SSL (Sandbox & Production) type. Follow the instructions. Once you are done, download it and double click to add it to your Keychain.
Next, you'll need to export your push certificate into p12 format. Please open the Keychain Access app, select the login keychain, and the My Certificates category. Search for your app ID and find the certificate starting with the Apple Push Services. Select both the certificate and its private key as shown in the screenshot below. Right click and choose Export 2 items... and save it. You're free to name the p12 file as you wish and to set up a passphrase or leave it empty.
Once you’ve downloaded your Auth Key or exported your certificate, you will need to upload it to your Countly Server. Please go to Management
> Applications
> Your App
. Scroll down to App settings and upload your Auth Key or exported certificate under the iOS settings section.
After filling all the required fields, click the Save changes button. Countly will check the validity of the credentials by initiating a test connection to the APNs.
Configuring iOS app
Using Countly Push Notifications on iOS apps is pretty straightforward. First, integrate the Countly iOS SDK as usual, if you still have yet to do so.
Then, under the Capabilities section of Xcode, enable Push Notifications and the Remote notifications Background Mode for your target, as shown in the screenshot below:
Now, start Countly in the application:didFinishLaunchingWithOptions:
method of your app with the following configuration. Do not forget to specify CLYPushNotifications
in the features
array on the CountlyConfig
object. Then you'll need to ask for user's permission for push notifications using the Countly askForNotificationPermission
method at any point in the app. The Countly iOS SDK will automatically handle the rest. No need to call any other method for registering when a device token is generated, or a push notification is received.
#import "Countly.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Start Countly with CLYPushNotifications feature as follows
CountlyConfig* config = CountlyConfig.new;
config.appKey = @"YOUR_APP_KEY";
config.host = @"https://YOUR_COUNTLY_SERVER";
config.features = @[CLYPushNotifications];
//config.pushTestMode = CLYPushTestModeDevelopment;
[Countly.sharedInstance startWithConfig:config];
//Ask for user's permission for Push Notifications (not necessarily here)
//You can do this later at any point in the app after starting Countly
[Countly.sharedInstance askForNotificationPermission];
// your code
return YES;
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
//Start Countly with CLYPushNotifications feature as follows
let config: CountlyConfig = CountlyConfig()
config.appKey = "YOUR_APP_KEY"
config.host = "https://YOUR_COUNTLY_SERVER"
config.features = [CLYPushNotifications]
//config.pushTestMode = CLYPushTestModeDevelopment
Countly.sharedInstance().start(with: config)
//Ask for user's permission for Push Notifications (not necessarily here)
//You can do this later at any point in the app after starting Countly
Countly.sharedInstance().askForNotificationPermission()
// your code
return true
}
Note: Ensure you code-sign your application using the explicit Provisioning Profile specific to your app's bundleID with an aps-environment key in it. You can get it from the iOS Provisioning Profiles section of the Apple Developer website. Be advised, wildcard (*) profiles or profiles aps-environment
key do not work with APNs, and the device can not receive a push token.
Note: Please make sure you do not set UNUserNotificationCenter.currentNotificationCenter
's delegate manually, as Countly iOS SDK will be acting as the delegate.
Note: To see how to send push notifications using the Countly Server, please check our Push Notifications documentation.
Deep links
When you send a push notification with custom actions buttons, you can redirect users to any custom page or view in your app by specifying deep links as custom actions button URLs. To do so, you will first need to create a URL scheme (e.g. : myapp://
) in your project.
To do so, select your app target in Xcode and open the Info
tab. Then, open the URL Types
section by clicking the horizontal arrow, and click the plus +
sign there.
Enter an identifier (preferably in reverse domain format) into the Identifier
field and enter your app's URL scheme (without ://
part) into the URL Schemes
field. Optionally, you can set an Icon
. You can leave the Role
field as whatever its default value is. When you are done, you can confirm that your new URL scheme has been added to your app's Info.plist
file. It should look like this:
After setting up the URL scheme, you should add the application:openURL:options:
method to your app delegate:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
{
//handle URL here to navigate to custom views
return YES;
}
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool
{
//handle URL here to navigate to custom views
return true
}
If your app's deployment target is lower than iOS9, you should add the application:openURL:sourceApplication:annotation:
method instead:
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation
{
//handle URL here to navigate to custom views
return YES;
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool
{
//handle URL here to navigate to custom views
return true
}
Then in this method, you can check the passed url
for custom view navigation using thescheme
and host
properties. For example, if you set the custom action button URLs as countly://productA
and countly://productB
, you can use something similar to this snippet:
if ([url.scheme isEqualToString: @"countly"])
{
if ([url.host isEqualToString: @"productA"])
{
// present view controller for Product A;
}
else if ([url.host isEqualToString: @"productB"])
{
// present view controller for Product B;
}
// or you can use host property directly
}
if (url.scheme == "countly")
{
if (url.host == "productA")
{
// present view controller for Product A;
}
else if (url.host == "productB")
{
// present view controller for Product B;
}
// or you can use host property directly
}
When users tap on the custom action buttons, the Countly iOS SDK will open the specified URLs with your app's scheme. Following this, the related method you added to your app's delegate will be called.
Rich Media
Rich push notifications allow you to send image, video, or audio attachments as well as customized action buttons on iOS10+. You will need to set up the Notification Service Extension to use it.
While the main project file is selected, please click the Editor > Add Target...
menu in Xcode, and add a Notification Service Extension
target.
Use the Product Name
field of the Notification Service Extension target as you wish (for example: CountlyNSE) and ensure the Team
is also selected.
Note: If Xcode asks a question about activating the scheme for a newly added Notification Service Extension target, you can select Cancel
.
Under the Build Phases
> Compile Sources
section of a newly added extension target, click the+
sign.
Select CountlyNotificationService.m
from the list.
Note: If you cannot see the CountlyNotificationService.m
file because you are using CocoaPods or Carthage for integration, please locate it yourself (probably under the Pods
folder) and add it to your project manually.
Then find the NotificationService.m
file (NotificationService.swift
in Swift projects) in the extension target. It is a default template file added automatically by Xcode. Import CountlyNotificationService.h
inside this file.
#import "CountlyNotificationService.h"
// No need to import files for Swift projects
Then add the following line at the end of the didReceiveNotificationRequest:withContentHandler:
method as shown below:
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler
{
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
//delete existing template code, and add this line
[CountlyNotificationService didReceiveNotificationRequest:request withContentHandler:contentHandler];
}
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void)
{
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
//delete existing template code, and add this line
CountlyNotificationService.didReceive(request, withContentHandler: contentHandler);
}
Note: Please ensure you also configure the App Transport Security
setting in the extension's Info.plist
file just as with the main application. Otherwise, media attachments from non-https sources cannot be loaded.
Note: Please ensure you check that the Deployment Target
version of the extension target is 10
, not 10.3 (or whatever minor version Xcode set automatically). Otherwise, users running iOS versions lower than the Deployment Target
value will not be able to get rich push notifications.
Provisional Permission for Push Notifications (iOS 12+ only)
iOS12 has a new feature called Provisional Permission for push notifications, and it is granted by default for all users. Without showing the notification permission dialog and without requiring users to accept anything, it allows you to send notifications to the users.
However, these notifications vary slightly, they do not actually notify the users. There are no alerts, no banners, no sounds, no badges. Nothing informing the users at the moment of notification delivery. Instead, these notifications go directly to the Notification Center and they silently pile up in the list. Only when the user goes to the Notification Center and checks the list, the user becomes aware of them.
To utilize Provisional Permission for push notifications (while requesting for notification permission types), you pass a new type, called UNAuthorizationOptionProvisional
.
UNAuthorizationOptions authorizationOptions = UNAuthorizationOptionProvisional;
[Countly.sharedInstance askForNotificationPermissionWithOptions:authorizationOptions completionHandler:^(BOOL granted, NSError *error)
{
NSLog(@"granted: %d", granted);
NSLog(@"error: %@", error);
}];
let authorizationOptions : UNAuthorizationOptions = [.provisional]
Countly.sharedInstance().askForNotificationPermission(options: authorizationOptions, completionHandler:
{ (granted : Bool, error : Error?) in
print("granted: \(granted)")
print("error: \(error)")
})
If this is the only notification permission type for which you ask, there will be no permission dialog and it will be granted by default. Then, later on, the Notification Center users can swipe on these provisional notifications and cancel the provisional permission anytime they please. The notification permission level then changes to the UNAuthorizationStatusDenied
from the UNAuthorizationStatusProvisional
state. This functions is a kind of opt-out.
How Push Notifications Work in Countly
When a push notification is received, the Countly iOS SDK handles everything automatically.
First, it checks if the notification payload has the Countly specific dictionary (c
key) and the notification ID inside it (i
key). If the Countly specific dictionary is present, it processes the notification. Otherwise, it does nothing. In both cases, the Countly iOS SDK forwards the notification to the default application delegate implementation for manual handling.
The processing of the notification payload depends on the iOS version, the application’s status (background or foreground) at the time of notification reception, and the notification payload's content.
If there is a media attachment or custom action buttons, the Notification Service Extension handles everything automatically. Users can view these notifications via their device’s 3D Touch or by swiping up on older devices.
When the app is not in the foreground, it waits for the user's interaction (e.g. tapping the actual notification or one of the custom action buttons). After the user's interaction, it automatically records a specific event indicating that that user has opened the push notification. If the user tapped one of the custom action buttons, it also records another specific event with button index segmentation and redirects them to the specified URL for that action.
When the app is in foreground, it uses UNNotificationPresentationOptionAlert
mode on iOS10+ to present a default notification banner and it uses the system UIAlertController
on older iOS versions and directly records push-opened events.
You can view the detailed flow in this chart (download the chart for a larger view):
Advanced Setup
Test Mode
For Development builds (where the project is code signed with an iOS Development Provisioning Profile), you should set pushTestMode
as CLYPushTestModeDevelopment
on CountlyConfig
object.
config.pushTestMode = CLYPushTestModeDevelopment;
config.pushTestMode = CLYPushTestModeDevelopment
For TestFlight or AdHoc Distribution builds (where the project is code signed with an iOS Distribution Provisioning Profile), you should set pushTestMode
as CLYPushTestModeTestFlightOrAdHoc
on CountlyConfig
object.
config.pushTestMode = CLYPushTestModeTestFlightOrAdHoc;
config.pushTestMode = CLYPushTestModeTestFlightOrAdHoc
So, you can send test push notifications to test devices on Countly Server Create Message screen.
Note: For App Store production builds, no need to set pushTestMode
on CountlyConfig
object at all.
Disabling Alerts Shown by Notifications
To disable messages from automatically being shown by the CLYPushNotifications
feature while the app is in the foreground, you can set the doNotShowAlertForNotifications
flag on the CountlyConfig
object. If set, no message will be displayed by using the default system UI in the app, but push-open events will be recorded automatically.
config.doNotShowAlertForNotifications = YES;
config.doNotShowAlertForNotifications = true
Manually Handling Notifications
If you would like to do additional custom work when a push notification is received, all you need to do is implement the default push-related methods in your application delegate (e.g. AppDelegate.m
). After finishing its internal work, the Countly iOS SDK will push forward the related method calls to the default implementations on the application delegate.
Please ensure you do not set theUNUserNotificationCenter.currentNotificationCenter
's delegate manually, as the Countly iOS SDK will be acting as the delegate. All you need to do is directly add theUNUserNotificationCenterDelegate
methods to your application delegate class.
Inside the push notification userInfo
dictionary you can find all the necessary information under the Countly Payload dictionary specified by the c
(kCountlyPNKeyCountlyPayload
) key. The array of the custom action buttons is specified by theb
(kCountlyPNKeyButtons
) key here, and each custom action button's title and action URL is specified by thet
(kCountlyPNKeyActionButtonTitle
) and l
(kCountlyPNKeyActionButtonURL
) keys, respectively. Here is an example of the Countly Push Notification Payload:
{
"aps":
{
"alert": "this is notification text",
//or {"title": "title string", "body": "message string"}, //if title is set separately
"sound": "sound_name", //if sound is set
"badge": 123, //if badge is set
"content-available": 1, //if data only flag is set
"mutable-content": 1, //if buttons or media attachment is set
},
"c":
{
"i": "100001", //notification ID
"l": "https://example.com/defaultlink", //if default link is set
"a": "https://rich.media.url.jpg", //if media attachment is set
"b": //if buttons is set
[
{
"t": "Custom Action Button Title 1", //button title
"l": "https://example.com/test1", //button link
},
{
"t": "Custom Action Button Title 2",
"l": "https://example.com/test2",
},
],
},
//any other custom data if set
}
You can create your own custom UI to display notification messages and custom action buttons according to your needs, along with URLs to redirect users when action is taken. Once users take action by clicking your custom buttons, you will need to manually report this event using this method:
NSDictionary* userInfo; // notification dictionary
NSInteger buttonIndex = 1; // clicked button index
// 1 for first action button
// 2 for second action button
// 0 for default action
[Countly.sharedInstance recordActionForNotification:userInfo clickedButtonIndex:buttonIndex];
let userInfo : Dictionary<String, AnyObject> = Dictionary() // notification dictionary
let buttonIndex : Int = 1 // clicked button index
// 1 for first action button
// 2 for second action button
// 0 for default action
Countly.sharedInstance().recordAction(forNotification:userInfo, clickedButtonIndex:buttonIndex)
Always Sending Push Tokens
Thanks to iOS’ Remote Notification Background Mode, silent push notifications can be sent to users who have not given notification permission. However, the Countly iOS SDK does not send push tokens to the server by default from users who have not given permission for notifications. You can change this by setting the sendPushTokenAlways
flag on the CountlyConfig
object. If set, push tokens from all users, regardless of their notification permission status, will be sent to the Countly Server and these users will be listed as possible recipients on the Create Message screen of the Countly Dashboard. Be advised; these users can not be notified by an alert, sound, or badge. This is useful only for sending data via silent notifications.
config.sendPushTokenAlways = YES;
config.sendPushTokenAlways = true
Notification Permission with Preferred Types and Callback
As asking for users’ permission for push notifications differ by iOS versions, the Countly iOS SDK has a one-liner convenience method, askForNotificationPermission
, which does this for both iOS10 and older versions. It simply asks for a user's permission for all available notification types. However, if you need to specify which notification types your app will use (alert, badge, sound) or if you need a callback to see a user's response to the permission dialog, you can use theaskForNotificationPermissionWithOptions:completionHandler:
method.
UNAuthorizationOptions authorizationOptions = UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert;
[Countly.sharedInstance askForNotificationPermissionWithOptions:authorizationOptions completionHandler:^(BOOL granted, NSError *error)
{
NSLog(@"granted: %d", granted);
NSLog(@"error: %@", error);
}];
let authorizationOptions : UNAuthorizationOptions = [.badge, .alert, .sound]
Countly.sharedInstance().askForNotificationPermission(options: authorizationOptions, completionHandler:
{ (granted : Bool, error : Error?) in
print("granted: \(granted)")
print("error: \(error)")
})
Custom Alert Sounds
The playing of notification sounds is handled by iOS, so all limitations would also be dictated by it. At the time of writing, audio data formats supported by iOS are: Linear PCM, MA4 (IMA/ADPCM), µLaw, aLaw. And that data needs to be packaged in aiff, wav, or caf file formats.
More information can be found here under "Preparing Custom Alert Sounds" section.
You have to add that audio file to your application target. If you have added an audio file called "exampleSound.wav" to your project, when sending a push notification, you should enter "exampleSound.wav" inside the "Send sound" field on Countly Server push notifications dashboard.
macOS launchNotification
launchNotification
property on initial configuration needs to be set in applicationDidFinishLaunching:
method of macOS apps that use CLYPushNotifications
feature, in order to handle app launches by push notification click.
config.launchNotification = notification;
Countly.launchNotification = notification
User Location
This feature is only available with an Enterprise Edition subscription.
Setting Location
Countly allows you to send GeoLocation-based push notifications to your users. By default, the Countly Server uses the GeoIP database to deduce a user's location. However, if your app has a better mean of detecting location, you can send this information to the Countly Server by using the initial configuration properties or relevant methods.
Initial configuration properties can be set on the CountlyConfig
object to be sent upon SDK initialization. These include:
-
location
: a CLLocationCoordinate2D struct specifying latitude and longitude -
ISOCountryCode
an NSString in ISO 3166-1 alpha-2 format country code -
city
an NSString specifying city name -
IP
an NSString specifying an IP address in IPv4 or IPv6 format
config.location = (CLLocationCoordinate2D){35.6895,139.6917};
config.city = @"Tokyo";
config.ISOCountryCode = @"JP";
config.IP = @"255.255.255.255"
config.location = CLLocationCoordinate2D(latitude:35.6895, longitude: 139.6917)
config.city = "Tokyo"
config.ISOCountryCode = "JP"
config.IP = "255.255.255.255"
GeoLocation info recording methods can also be called at any time after the Countly iOS SDK has started. Values recorded using these methods will override the values specified upon initial configuration.
[Countly.sharedInstance recordLocation:(CLLocationCoordinate2D){35.6895,139.6917}];
[Countly.sharedInstance recordCity:@"Tokyo" andISOCountryCode:@"JP"];
[Countly.sharedInstance recordIP:@"255.255.255.255"];
Countly.sharedInstance().recordLocation(CLLocationCoordinate2D(latitude:33.6895, longitude:139.6917))
Countly.sharedInstance().recordCity("Tokyo", andISOCountryCode:"JP")
Countly.sharedInstance().recordIP("255.255.255.255")
Preferably you should use either location coordinate or city and country code pair.
Disabling Location
Also during init, you can disable location:
config.disableLocation = YES;
config.disableLocation = true
GeoLocation info can also be disabled after init:
[Countly.sharedInstance disableLocationInfo];
Countly.sharedInstance().disableLocationInfo;
Once disabled, you can re-enable GeoLocation info by calling the recordLocation:
or recordCity:andISOCountryCode:
or recordIP:
method.
Remote Config
Remote config allows you to modify how your app functions or looks by requesting key-value pairs from your Countly server. The returned values may be modified based on the user properties. For more details, please see the Remote Config documentation.
Once downloaded, Remote config values will be saved persistently and available on your device between app restarts unless they are erased.
The two ways of acquiring remote config data are enabling automatic download triggers or manual requests.
If a full download of remote config values is performed, the previous list of values is replaced with the new one. If a partial download is performed, only the retrieved keys are updated, and values that are not part of that download stay as they were. A previously valid key may return no value after a full download.
Downloading Values
Automatic Remote Config Triggers
Automatic remote config triggers have been turned off by default; therefore, no remote config values will be requested without developer intervention.
The automatic download triggers that would trigger a full value download are:
- when the SDK has finished initializing
- after the device ID is changed without merging
- when user gets out of temp ID mode
- when 'remote-config' consent is given after it had been removed before (if consents are enabled)
To enable the automatic triggers, you have to call enableRemoteConfigAutomaticTriggers
on the configuration object you will provide during init.
config.enableRemoteConfigAutomaticTriggers = YES;
config.enableRemoteConfigAutomaticTriggers = true
Another thing you can do is to enable value caching with the enableRemoteConfigValueCaching
flag. If all values were not updated, you would have metadata indicating if a value belongs to the old or current user.
config.enableRemoteConfigValueCaching = YES;
config.enableRemoteConfigValueCaching = true
Manual Calls
There are three ways to trigger remote config value download manually:
- Manually downloading all keys
- Manually downloading specific keys
- Manually downloading, omitting (everything except) keys.
Each of these calls also has an optional parameter that you can provide a RCDownloadCallback to, which would be triggered when the download attempt has finished.
downloadKeys
is the same as the automatically triggered update - it replaces all stored values with the ones from the server (all locally stored values are deleted and replaced with new ones).
[Countly.sharedInstance.remoteConfig downloadKeys:^(CLYRequestResult _Nonnull response, NSError * _Nonnull error, BOOL fullValueUpdate, NSDictionary<NSString *,CountlyRCData *> * _Nonnull downloadedValues) {
//...
}];
Countly.sharedInstance().remoteConfig.downloadKeys { response, error, fullValueUpdate, downloadedValues in
//...
}
Or you might only want to update specific key values. To do so, you will need to call downloadSpecificKeys
to downloads new values for the wanted keys. Those are provided with a String array.
[Countly.sharedInstance.remoteConfig downloadSpecificKeys:NSArray *keys completionHandler:^(CLYRequestResult _Nonnull response, NSError * _Nonnull error, BOOL fullValueUpdate, NSDictionary<NSString *,CountlyRCData *> * _Nonnull downloadedValues) {
//...
}];
Countly.sharedInstance().remoteConfig.downloadSpecificKeys(keys, completionHandler: { response, error, fullValueUpdate, downloadedValues in
//...
})
Or you might want to update all the values except a few defined keys. To do so, call downloadOmittingKeys
would update all values except the provided keys. The keys are provided with a String array.
[Countly.sharedInstance.remoteConfig downloadOmittingKeys:NSArray *omitKeys completionHandler:^(CLYRequestResult _Nonnull response, NSError * _Nonnull error, BOOL fullValueUpdate, NSDictionary<NSString *,CountlyRCData *> * _Nonnull downloadedValues) {
//...
}];
Countly.sharedInstance.remoteConfig.downloadOmittingKeys(omitKeys, completionHandler: { response, error, fullValueUpdate, downloadedValues in
//...
})
When making requests with an "inclusion" or "exclusion" array, if those arrays are empty or null, they will function the same as an update all request and will update all the values. This means it will also erase all keys not returned by the server.
Accessing Values
To get a stored value, call getValue
with the specified key. This returns an CountlyRCData object that contains the value of the key and the metadata about that value's owner. If value in CountlyRCData was null
then no value was found or the value was null
.
id value_1 = [Countly.sharedInstance.remoteConfig getValue:@"key_1"].value;
id value_2 = [Countly.sharedInstance.remoteConfig getValue:@"key_2"].value;
id value_3 = [Countly.sharedInstance.remoteConfig getValue:@"key_3"].value;
id value_4 = [Countly.sharedInstance.remoteConfig getValue:@"key_4"].value;
int intValue = [value_1 isKindOfClass:[NSNumber class]] ? [(NSNumber *)value_1 intValue] : 0;
double doubleValue = [value_2 isKindOfClass:[NSNumber class]] ? [(NSNumber *)value_2 doubleValue] : 0.0;
NSArray *jArray = [value_3 isKindOfClass:[NSArray class]] ? (NSArray*)value_3 : @[];
NSDictionary *jObj = [value_4 isKindOfClass:[NSDictionary class]] ? (NSDictionary*)value_4 : @{};
let value_1 = Countly.sharedInstance().remoteConfig.getValue("key_1")?.value
let value_2 = Countly.sharedInstance().remoteConfig.getValue("key_2")?.value
let value_3 = Countly.sharedInstance().remoteConfig.getValue("key_3")?.value
let value_4 = Countly.sharedInstance().remoteConfig.getValue("key_4")?.value
let intValue = value_1 as? Int ?? 0
let doubleValue = value_2 as? Double ?? 0.0
let jArray = value_3 as? [Any] ?? []
let jObj = value_4 as? [String: Any] ?? [:]
If you want to get all values together you can use getAllValues
which returns an NSDictionary<NSString *, CountlyRCData*>. The SDK does not know the returned value type, so, it will return the Any
. The developer then needs to cast it to the appropriate type. The returned values may also be JSONArray
, JSONObject
, or just a simple value, such as NSNumber
.
NSDictionary<NSString*, CountlyRCData*> *allValues = [Countly.sharedInstance.remoteConfig getAllValues];
int intValue = [(NSNumber *)allValues[@"key_1"] intValue];
double doubleValue = [(NSNumber *)allValues[@"key_2"] doubleValue];
NSArray*jArray = (NSArray *)allValues[@"key_3"];
NSDictionary*jObj = (NSDictionary *)allValues[@"key_4"];
let allValues = Countly.sharedInstance().remoteConfig.getAllValues()
let intValue = (allValues["key_1"] as? NSNumber)?.intValue ?? 0
let doubleValue = (allValues["key_2"] as? NSNumber)?.doubleValue ?? 0.0
let jArray = allValues["key_3"] as? [Any] ?? []
let jObj = allValues["key_4"] as? [String: Any] ?? [:]
CountlyRCData object has two keys: value (Any) and isCurrentUsersData (Bool). Value holds the data sent from the server for the key that the CountlyRCData object belongs to. The isCurrentUsersData is only false when there was a device ID change, but somehow (or intentionally) a remote config value was not updated.
@interface CountlyRCData : NSObject
@property (nonatomic) id value;
@property (nonatomic) BOOL isCurrentUsersData;
@end
class CountlyRCData {
var value: Any
var isCurrentUsersData: Bool
}
Clearing Stored Values
At some point, you might like to erase all the values downloaded from the server. You will need to call one function to do so.
[Countly.sharedInstance.remoteConfig clearAll];
Countly.sharedInstance().remoteConfig.clearAll()
Global Download Callbacks
Also, you may provide callback functions to be informed when the request is finished with remoteConfigRegisterGlobalCallback
method during SDK initialization:
[config remoteConfigRegisterGlobalCallback:^(CLYRequestResult _Nonnull response, NSError * _Nonnull error, BOOL fullValueUpdate, NSDictionary<NSString *,CountlyRCData *> * _Nonnull downloadedValues) {
// ...
}]
config.remoteConfigRegisterGlobalCallback { response, error, fullValueUpdate, downloadedValues in
// ...
}
RCDownloadCallback is called when the remote config download request is finished, and it would have the following parameters:
-
response
: CLYRequestResult Enum (either CLYResponseError, CLYResponseSuccess or CLYResponseNetworkIssue) -
error
: NSError (error message. "null" if there is no error) -
fullValueUpdate
: BOOL ("true" - all values updated, "false" - a subset of values updated) -
downloadedValues
: NSDictionary<NSString *,CountlyRCData*> (the whole downloaded remote config values)
typedef void (^RCDownloadCallback)(CLYRequestResult response, NSError *_Nullable error, BOOL fullValueUpdate, NSDictionary<NSString*, CountlyRCData *>* downloadedValues);
typealias RCDownloadCallback = (CLYRequestResult, Error?, Bool, [String: CountlyRCData]?) -> Void
downloadedValues
would be the downloaded remote config data where the keys are remote config keys, and their value is stored in CountlyRCData class with metadata showing to which user data belongs. The data owner will always be the current user if caching is not enabled.
You can also register (or remove) RCDownloadCallbacks to do different things after the SDK initialization. You can register callbacks multiple times:
// register a callback
[Countly.sharedInstance.remoteConfig registerDownloadCallback:^(CLYRequestResult _Nonnull response, NSError *_Nonnull error, BOOL fullValueUpdate, NSDictionary<NSString *,CountlyRCData*> * _Nonnull downloadedValues) {
//...
}];
// remove a callback
[Countly.sharedInstance.remoteConfig removeDownloadCallback:^(CLYRequestResult _Nonnull response, NSError *_Nonnull error, BOOL fullValueUpdate, NSDictionary<NSString *,CountlyRCData*> * _Nonnull downloadedValues) {
//...
}];
// register a callback
Countly.sharedInstance().remoteConfig.removeDownloadCallback{ response, error, fullValueUpdate, downloadedValues in
//...
}
// remove a callback
Countly.sharedInstance().remoteConfig.removeDownloadCallback{ response, error, fullValueUpdate, downloadedValues in
//...
}
A/B Testing
You can enroll your users into into A/B tests for certain keys or remove them from some or all existing A/B tests available.
Enrollment on Download
You can enroll to available experiments when downloading the Remote Config values automatically. To do this you should call enrollABOnRCDownload
method on the configuration object you pass for the initialization:
config.enrollABOnRCDownload= YES;
Enrollment on Access
You can also enroll to A/B tests while getting RC values from storage. You can use getValueAndEnroll
while getting a single value and getAllValuesAndEnroll
while getting all values to enroll to the keys that exist. If no value was stored for those keys these functions would not enroll the user. Both of these functions works the same way with their non-enrolling variants, namely; getValue
and getAllValues
.
Enrollment on Action
To enroll a user into the A/B tests for the given keys you use the following method:
[Countly.sharedInstance.remoteConfig enrollIntoABTestsForKeys:NSArray *keys];
Countly.sharedInstance().remoteConfig.enrollIntoABTests(forKeys: keys as [String])
Here the keys array is the mandatory parameter for this method to work.
Exiting A/B Tests
If you want to remove users from A/B tests of certain keys you can use the following function:
[Countly.sharedInstance.remoteConfig exitABTestsForKeys:NSArray *keys];
Countly.sharedInstance().remoteConfig.exitABTestsForKeys(forKeys: keys as [String])
Here if no keys are provided it would remove the user from all A/B tests instead.
User Feedback
Ratings
Star Rating dialog
Optionally, you can set the Countly iOS SDK to automatically ask users for a 1 to 5-star rating, depending on the app launch count for each version. To do so, you will need to set the starRatingSessionCount
property on the CountlyConfig
object. When the total number of sessions reaches the starRatingSessionCount
, an alert view asking for a 1 to 5-star rating will be displayed automatically, once for each new version of the app.
config.starRatingSessionCount = 10;
config.starRatingSessionCount = 10
If you would like the star-rating dialog to only be displayed once per app lifetime, instead of for each new version, you can set the starRatingDisableAskingForEachAppVersion
flag on the CountlyConfig
object.
config.starRatingDisableAskingForEachAppVersion = YES;
config.starRatingDisableAskingForEachAppVersion = true
Additionally, you can customize the star-rating dialog message using the starRatingMessage
property on the CountlyConfig
object. If you do not explicitly specify this property, the message will read, "How would you rate the app?" or a corresponding localized version depending on the device language. Currently supported localizations: English, Turkish, Japanese, Chinese, Russian, Czech, Latvian, and Bengali.
config.starRatingMessage = @"Please rate our app?";
config.starRatingMessage = "Please rate our app?"
config.starRatingDismissButtonTitle = "No, thanks."
Additionally, you can set the starRatingCompletion
block property on the CountlyConfig
object to be executed after the star-rating dialog has been automatically shown. The completion block has a single NSInteger parameter that indicates the 1 to 5-star rating given by the user. If the user dismissed the dialog without giving a rating, the value for this rating will be 0, and it will not be reported to the server.
config.starRatingCompletion = ^(NSInteger rating)
{
NSLog(@"rating %d",(int)rating);
};
config.starRatingCompletion = { (rating : Int) in print("rating \(rating)") }
Additionally, you can use the askForStarRating:
method to ask for a star rating anytime you would like. It displays the 1 to 5-star rating dialog manually and executes the completion block after the user's action. The completion block takes a single NSInteger parameter that indicates the 1 to 5-star rating given by the user. If the user dismissed the dialog without giving a rating, the value for this rating will be 0, and it will not be reported to the server. Manually asking for a star rating does not affect the automatically requested nature of the star rating.
[Countly.sharedInstance askForStarRating:^(NSInteger rating)
{
NSLog(@"rating %li",(long)rating);
}];
Countly.sharedInstance().ask(forStarRating:{ (rating : Int) in print("rating \(rating)") })
Rating Widget
You can use the Countly iOS SDK to display ratings feedback widgets configured on the Countly Server. For more information on ratings feedback widgets, please visit the Ratings widget documentation.
Here is how you can utilize ratings feedback widgets in your iOS apps:
Once you call the presentRatingWidgetWithID:completionHandler:
method, the ratings feedback widget with the given ID will be displayed in a WKWebView, having been placed in the UIViewController.
First, the availability of the ratings feedback widget will be checked asynchronously. If the ratings feedback widget is available, it will be modally presented. Otherwise, the completionHandler
will be called with an NSError
. The completionHandler
will also be called with nil
when the ratings feedback widget is dismissed by the user.
[Countly.sharedInstance presentRatingWidgetWithID:@"RATINGS_FEEDBACK_WIDGET_ID" completionHandler:^(NSError* error)
{
if (error)
NSLog(@"Ratings feedback widget presentation failed: \n%@\n%@", error.localizedDescription, error.userInfo);
else
NSLog(@"Ratings feedback widget presented successfully");
}];
Countly.sharedInstance().presentRatingWidget(withID: "RATINGS_FEEDBACK_WIDGET_ID", completionHandler:
{ (error : Error?) in
if (error != nil)
{
print("Ratings feedback widget presentation failed: \n \(error!.localizedDescription) \n \((error! as NSError).userInfo)")
}
else
{
print("Ratings feedback widget presented successfully");
}
})
Manual Rating Reporting
If you wish to construct your own custom UI for displaying ratings widgets, you can manually record the result with the widget ID and other parameters as follows:widgetID
: ID of the rating widget created on Countly Serverrating
: User's ratingemail
: User's e-mail address (optional)comment
: User's comment (optional)userCanBeContacted
: User's consent for whether they can be contacted via e-mail or not
[Countly.sharedInstance recordRatingWidgetWithID:@"RATINGS_FEEDBACK_WIDGET_ID" rating:4 email:@"email@example.com" comment:@"Some comment" userCanBeContacted:YES];
Countly.sharedInstance().recordRatingWidget(withID: "RATINGS_FEEDBACK_WIDGET_ID", rating: 4, email: "email@example.com", comment: "Some comment", userCanBeContacted: true)
Calls to this method will be ignored if:
- Consent for @c CLYConsentFeedback is not given, while @c requiresConsent flag is set on initial configuration.
- widgetID
is not a non-zero length valid string.
Feedback Widget
Here is how you can utilize NPS (Net Promoter Score) and survey feedback widgets in your iOS apps: First you need to get the list of all available NPS and survey widgets:
[Countly.sharedInstance getFeedbackWidgets:^(NSArray * feedbackWidgets, NSError * error)
{
if (error)
{
NSLog(@"Getting widgets list failed. Error: %@", error);
}
else
{
NSLog(@"Getting widgets list successfully completed. %@", [feedbackWidgets description]);
}
}];
Countly.sharedInstance().getFeedbackWidgets
{ (feedbackWidgets: [CountlyFeedbackWidget], error) in
if (error != nil)
{
print("Getting widgets list failed. \n \(error!.localizedDescription) \n \((error! as NSError).userInfo)")
}
else
{
print("Getting widgets list successfully completed. \(String(describing: feedbackWidgets))")
}
}
When feedback widgets are fetched successfully, completionHandler
will be executed with an array of CountlyFeedbackWidget
objects. Otherwise, completionHandler
will be executed with an NSError
.
Calls to this method will be ignored and completionHandler
will not be executed if:
- Consent for CLYConsentFeedback
is not given, while requiresConsent
flag is set on initial configuration.
- Current device ID is CLYTemporaryDeviceID
.
Once you get the list, you can inspect CountlyFeedbackWidget
objects in it, by checking their type
, ID
, and name
properties to decide which one to display. And when you decide, you can use present
method on CountlyFeedbackWidget
object to display it.
CountlyFeedbackWidget * aFeedbackWidget = feedbackWidgets.firstObject; //assuming we want to display the first one in the list
[aFeedbackWidget present];
let aFeedbackWidget : CountlyFeedbackWidget? = feedbackWidgets?.first //assuming we want to display the first one in the list
aFeedbackWidget?.present()
Optionally you can pass appear and dismiss callback blocks:
CountlyFeedbackWidget * aFeedbackWidget = feedbackWidgets.firstObject; //assuming we want to display the first one in the list
[aFeedbackWidget presentWithAppearBlock:^
{
NSLog(@"Appeared!");
}
andDismissBlock:^
{
NSLog(@"Dismissed!");
}];
let aFeedbackWidget : CountlyFeedbackWidget? = feedbackWidgets?.first //assuming we want to display the first one in the list
aFeedbackWidget?.present(appear:
{
print("Appeared.")
},
andDismiss:
{
print("Dismissed.")
})
Manual Reporting
Optionally you can fetch feedback widget data and create your own UI using:
CountlyFeedbackWidget * aFeedbackWidget = feedbackWidgets.firstObject; //assuming we want to display the first one in the list
[aFeedbackWidget getWidgetData:^(NSDictionary * widgetData, NSError * error)
{
if (error)
{
NSLog(@"Getting widget data failed. Error: %@", error);
}
else
{
NSLog(@"Getting widget data successfully completed. %@", [widgetData description]);
}
}];
let aFeedbackWidget : CountlyFeedbackWidget? = feedbackWidgets?.first //assuming we want to display the first one in the list
aFeedbackWidget?.getData
{ (widgetData: [AnyHashable: Any]?, error: Error?) in
if (error != nil)
{
print("Getting widget data failed. Error \(error.debugDescription)")
}
else
{
print("Getting widget data successfully completed. \(String(describing: widgetData))")
}
}
And once you are done with your custom feedback widget UI you can record the result:
[aFeedbackWidget recordResult:resultDictionary];
// or
[aFeedbackWidget recordResult:nil]; // if user dismissed the feedback widget without completing it
aFeedbackWidget.recordResult(resultDictionary)
// or
aFeedbackWidget.recordResult(nil) // if user dismissed the feedback widget without completing it
For more information regarding how to structure the result dictionary, you would look here.
User Profiles
This feature is only available with an Enterprise Edition subscription.
You can see detailed user information under the User Profiles section of the Countly Dashboard by recording user properties.
Default User Properties
You can record default user detail properties by adhering to the following:
//default properties
Countly.user.name = @"John Doe";
Countly.user.username = @"johndoe";
Countly.user.email = @"john@doe.com";
Countly.user.birthYear = @1970;
Countly.user.organization = @"United Nations";
Countly.user.gender = @"M";
Countly.user.phone = @"+0123456789";
//profile photo
Countly.user.pictureURL = @"https://s12.postimg.org/qji0724gd/988a10da33b57631caa7ee8e2b5a9036.jpg";
//or local image on the device
Countly.user.pictureLocalPath = localImagePath;
//save
[Countly.user save];
//default properties
Countly.user().name = "John Doe" as CountlyUserDetailsNullableString
Countly.user().username = "johndoe" as CountlyUserDetailsNullableString
Countly.user().email = "john@doe.com" as CountlyUserDetailsNullableString
Countly.user().birthYear = 1970 as CountlyUserDetailsNullableNumber
Countly.user().organization = "United Nations" as CountlyUserDetailsNullableString
Countly.user().gender = "M" as CountlyUserDetailsNullableString
Countly.user().phone = "+0123456789" as CountlyUserDetailsNullableString
//profile photo
Countly.user().pictureURL = "https://s12.postimg.org/qji0724gd/988a10da33b57631caa7ee8e2b5a9036.jpg" as CountlyUserDetailsNullableString
//or local image on the device
Countly.user().pictureLocalPath = localImagePath as CountlyUserDetailsNullableString
//save
Countly.user().save()
Note: Local images specified on the pictureLocalPath
property will not be persisted exclusively. If a request fails and is retried later, the local image is expected to still be present on the exact same path. Otherwise, the upload will be aborted.
Custom User Properties
You can record custom user detail properties by adhering to the following:
//custom properties
Countly.user.custom = @{@"testkey1": @"testvalue1", @"testkey2": @"testvalue2"};
//save
[Countly.user save];
//custom properties
Countly.user().custom = ["testkey1": "testvalue1", "testkey2": "testvalue2"] as CountlyUserDetailsNullableDictionary
//save
Countly.user().save()
Custom User Property Modifiers
Also, you can use custom user property modifiers, such as the following:
[Countly.user set:@"key101" value:@"value101"];
[Countly.user setOnce:@"key101" value:@"value101"];
[Countly.user unSet:@"key101"];
[Countly.user increment:@"key102"];
[Countly.user incrementBy:@"key102" value:5];
[Countly.user multiply:@"key102" value:2];
[Countly.user max:@"key102" value:30];
[Countly.user min:@"key102" value:20];
[Countly.user push:@"key103" value:@"singlevalue"];
[Countly.user push:@"key103" values:@[@"a",@"b",@"c",@"d"]];
[Countly.user pull:@"key103" value:@"b"];
[Countly.user pull:@"key103" values:@[@"a",@"d"]];
[Countly.user pushUnique:@"key104" value:@"uniqueValue"];
[Countly.user pushUnique:@"key104" values:@[@"uniqueValue2",@"uniqueValue3"]];
//save
[Countly.user save];
Countly.user().set("key101", value:"value101")
Countly.user().setOnce("key101", value:"value101")
Countly.user().unSet("key101")
Countly.user().increment("key102")
Countly.user().increment(by:"key102", value:5)
Countly.user().multiply("key102", value:2)
Countly.user().max("key102", value:30)
Countly.user().min("key102", value:20)
Countly.user().push("key103", value:"singlevalue")
Countly.user().push("key103", values:["a","b","c","d"])
Countly.user().pull("key103", value:"b")
Countly.user().pull("key103", values:["a","d"])
Countly.user().pushUnique("key104", value:"uniqueValue")
Countly.user().pushUnique("key104", values:["uniqueValue2","uniqueValue3"])
//save
Countly.user().save()
Note:Once saved, all properties on Countly.user
will be cleared.
Note:You can start setting user properties even before starting the Countly iOS SDK. They will be saved automatically when the SDK is started.
Orientation Tracking
You can set the enableOrientationTracking
flag on theCountlyConfig
object before starting Countly. This flag is used for enabling automatic user interface orientation tracking. If set, user interface orientation tracking feature will be enabled and an event will be sent whenever user interface orientation changes. Orientation event will not be sent if consent for CLYConsentUserDetails
is not given while requiresConsent
flag is set on initial configuration. Automatic user interface orientation tracking is enabled by default. For disabling it, please set this flag to NO
.
config.enableOrientationTracking = YES;
config.enableOrientationTracking = true
Application Performance Monitoring
Performance Monitoring feature allows you to analyze your application's performance on various aspects. For more details please see Performance Monitoring documentation.
Here is how you can utilize Performance Monitoring feature in your iOS apps:
First, you need to enable Performance Monitoring feature on the initial configuration:
config.enablePerformanceMonitoring = YES;
config.enablePerformanceMonitoring = true
With this, Countly iOS SDK will start measuring some performance traces automatically. Those include app start time, app foreground time, app background time. Additionally, custom traces and network traces can be manually recorded.
App Start Time
For the app start time to be recorded, you need to call appLoadingFinished
method.
It calculates and records the app launch time for performance monitoring.
It should be called when the app is loaded and it successfully displayed its first user-facing view. E.g. viewDidAppear:
method of the root view controller or whatever place is suitable for the app's flow. Time passed since the app has started to launch will be automatically calculated and recorded for performance monitoring. App launch time can be recorded only once per app launch. So, the second and following calls to this method will be ignored.
[Countly.sharedInstance appLoadingFinished];
Countly..sharedInstance().appLoadingFinished()
Manual Network Traces
You can record manual network traces using therecordNetworkTrace:requestPayloadSize:responsePayloadSize:responseStatusCode:startTime:endTime:
method.
A network trace is a collection of measured information about a network request.
When a network request is completed, a network trace can be recorded manually to be analyzed in the Performance Monitoring feature later with the following parameters:
- traceName
: A non-zero length valid string
- requestPayloadSize
: Size of the request's payload in bytes
- responsePayloadSize
: Size of the received response's payload in bytes
- responseStatusCode
: HTTP status code of the received response
- startTime
: UNIX time stamp in milliseconds for the starting time of the request
- endTime
: UNIX time stamp in milliseconds for the ending time of the request
[Countly.sharedInstance recordNetworkTrace:@"/test/endpoint" requestPayloadSize:3445 responsePayloadSize:1290 responseStatusCode:200 startTime:1593418666954 endTime:1593418667384];
Countly.sharedInstance().recordNetworkTrace("/test/api", requestPayloadSize:3445, responsePayloadSize:1290, responseStatusCode:200, startTime:1593418666954, endTime:1593418667384)
Custom Traces
You can also measure any operation you want and record it using custom traces. First, you need to start a trace by using the startCustomTrace
method:
[Countly.sharedInstance startCustomTrace:@"unzipping_saved_files"];
Countly.sharedInstance().startCustomTrace("unzipping_saved_files")
Then you can end it using the endCustomTrace:metrics:
method, optionally passing any metrics as key-value pairs:
[Countly.sharedInstance endCustomTrace:@"unzipping_saved_files" metrics:@{@"total_file_size": @1655700}];
Countly.sharedInstance().startCustomTrace("unzipping_saved_files", metrics:["total_file_size": 1655700])
The duration of the custom trace will be automatically calculated on ending. Trace names should be non-zero length valid strings. Trying to start a custom trace with the already started name will have no effect. Trying to end a custom trace with already ended (or not yet started) name will have no effect.
You can also cancel any custom trace you started, using cancelCustomTrace:
method:
[Countly.sharedInstance cancelCustomTrace:@"unzipping_saved_files"];
Countly.sharedInstance().cancelCustomTrace("unzipping_saved_files")
Additionally, if you need you can cancel all custom traces you started, using the clearAllCustomTraces
method:
User Consent
For compatibility with data protection regulations, such as GDPR, the Countly iOS SDK allows developers to enable/disable any feature at any time depending on user consent.
More information about GDPR can be found here.
Feature Names
Currently, available features with consent control are as follows:
CLYConsentSessions
CLYConsentEvents
CLYConsentUserDetails
CLYConsentCrashReporting
CLYConsentPushNotifications
CLYConsentLocation
CLYConsentViewTracking
CLYConsentAttribution
CLYConsentPerformanceMonitoring
CLYConsentFeedback
CLYConsentRemoteConfig
Setup During Init
The requirement for consent is disabled by default. To enable it, you will have to set the requiresConsent
flag true
upon initial configuration to utilize consents.
config.requiresConsent = YES;
config.requiresConsent = true
With this flag set, the Countly iOS SDK will not automatically collect or send any data and will ignore all manual calls. Until explicit consent is given for a feature, it will remain inactive. After consent for a feature is given, it will launch immediately and will remain active.
You can provide specific consents during initialization by using the consents
array on the CountlyConfig
object before starting Countly
config.consents = @[CLYConsentSessions, CLYConsentEvents];
config.consents = [CLYConsentSessions, CLYConsentEvents];
Or, if you would like to give consent for all the features during initialization, you can set enableAllConsents
flag:
You can also change the consents after initializing the SDK.
To give consent for a feature, you can use the giveConsentForFeature:
method by passing the feature name:
[Countly.sharedInstance giveConsentForFeature:CLYConsentSessions];
[Countly.sharedInstance giveConsentForFeature:CLYConsentEvents];
Countly.sharedInstance().giveConsent(forFeature: CLYConsentSessions)
Countly.sharedInstance().giveConsent(forFeature: CLYConsentEvents)
Or, you can give consent for more than one feature at a time using the giveConsentForFeatures:
method or consents
property on the CountlyConfig
object, by passing the feature names as an NSArray
:
[Countly.sharedInstance giveConsentForFeatures:@[CLYConsentSessions, CLYConsentEvents];
Countly.sharedInstance().giveConsent(forFeatures: [CLYConsentSessions, CLYConsentEvents])
Or, if you would like to give consent for all the features, you can use the giveAllConsents
convenience method:
[Countly.sharedInstance giveAllConsents];
Countly.sharedInstance().giveAllConsents()
The Countly iOS SDK does not persistently store the status of given consents. You are expected to handle receiving consent from end-users using proper UIs depending on your app's context. You are also expected to store them either locally or remotely. Following this step, you will need to call the ‘giving consent’ methods on each app launch, right after starting the Countly iOS SDK depending on the permissions you managed to get from the end-users.
If the end-user changes his/her mind about consents at a later time, you will need to reflect this in the Countly iOS SDK using the cancelConsentForFeature:
method:
[Countly.sharedInstance cancelConsentForFeature:CLYConsentSessions];
[Countly.sharedInstance cancelConsentForFeature:CLYConsentEvents];
Countly.sharedInstance().cancelConsent(forFeature: CLYConsentSessions)
Countly.sharedInstance().cancelConsent(forFeature: CLYConsentEvents)
Or, you can cancel consent for more than one feature at a time using the cancelConsentForFeatures:
method by passing the feature names as an NSArray
:
[Countly.sharedInstance cancelConsentForFeatures:@[CLYConsentSessions, CLYConsentEvents];
Countly.sharedInstance().cancelConsent(forFeatures: [CLYConsentSessions, CLYConsentEvents])
Or, if you would like to cancel consent for all the features, you can use the cancelConsentForAllFeatures
convenience method:
[Countly.sharedInstance cancelConsentForAllFeatures];
Countly.sharedInstance().cancelConsentForAllFeatures()
Once consent for a feature has been cancelled, that feature is stopped immediately and kept inactive from that point onward.
The Countly iOS SDK reports consent changes to the Countly Server, so that the Countly Server can make preparations, or clean-up on the server side as well.
Security and Privacy
You can specify extra security features on the CountlyConfig
object:
Parameter Tamper Protection
You can set the optional secretSalt
to be used for calculating the checksum of the request data which will be sent with each request using the &checksum256
field. You will need to set the exact same secretSalt
on the Countly Server. If the secretSalt
on the Countly Server is set, all requests would be checked for validity of the &checksum256
field before being processed.
config.secretSalt = @"mysecretsalt";
config.secretSalt = "mysecretsalt"
SSL Certificate Pinning
You can use optional pinnedCertificates
on the CountlyConfig
object for specifying bundled certificates to be used for public key pinning. Certificates from your Countly Server must be DER encoded and should have a .der
, .cer
or .crt
extension. They must also be added to your project and be included in the Copy Bundles Resources.
config.pinnedCertificates = @[@"mycertificate.cer"];
config.pinnedCertificates = ["mycertificate.cer"]
Using a Self Signed-Server Certificate
You can set the shouldIgnoreTrustCheck
flag on theCountlyConfig
object before starting Countly.
This flag is used for ignoring all SSL trust check for pinned certificates by setting server trust as an exception.
It can be used for self-signed certificates and works only for Development environment where DEBUG
flag is set in target's build settings.
config.shouldIgnoreTrustCheck = YES;
config.shouldIgnoreTrustCheck = true
Other Features and Notes
Changing Host and App Key
App Key
Changing App Key
You can configure the current app key after you started the SDK using setNewAppKey:
method:
[Countly.sharedInstance setNewAppKey:@"NewAppKey"];
Countly.sharedInstance().setNewAppKey("NewAppKey")
The new app key will be used for all the new requests after setting it. Requests already queued previously, will keep using the old app key. Before switching to new app key, this method suspends the SDK and resumes it immediately after the new key is set. The new app key needs to be a non-zero length string, otherwise the method call is ignored. recordPushNotificationToken
and updateRemoteConfigWithCompletionHandler:
methods may need to be manually called again after the app key change.
Replacing App Keys in Queue
You can replace different app keys in the request queue with the current app key using replaceAllAppKeysInQueueWithCurrentAppKey
method:
[Countly.sharedInstance replaceAllAppKeysInQueueWithCurrentAppKey];
Countly.sharedInstance().replaceAllAppKeysInQueueWithCurrentAppKey()
In request queue, if there are any request whose app key is different than the current app key, these requests' app key will be replaced with the current app key.
Removing App Keys from Queue
You can remove requests whose app key is different than the current app key in the request queue using removeDifferentAppKeysFromQueue
method:
[Countly.sharedInstance removeDifferentAppKeysFromQueue];
Countly.sharedInstance().removeDifferentAppKeysFromQueue()
Host
Changing Host
You can configure the host even after you started the SDK using setNewHost:
method:
[Countly.sharedInstance setNewHost:@"https://example.com"];
Countly.sharedInstance().setNewHost("https://example.com")
Requests that are already queued before changing the host will also be using the new host. The new host needs to be a non-zero length string, otherwise it is ignored. recordPushNotificationToken
and updateRemoteConfigWithCompletionHandler:
methods may need to be manually called again after the host change.
You can further specify your optional settings on the CountlyConfig
:
Event Send Threshold
You can specify the eventSendThreshold
on the CountlyConfig
object before starting Countly. It is used to send events requests to the server when the number of recorded events reaches the threshold without waiting for the next update session request. If the eventSendThreshold
is not explicitly set, the default setting will be at 10 for iOS, tvOS & macOS, and 3 for watchOS.
config.eventSendThreshold = 5;
config.eventSendThreshold = 5
Setting Maximum Request Queue Size
You can specify the storedRequestsLimit
on the CountlyConfig
object before starting Countly. It is used to limit the number of requests in the request queue when there is a Countly Server connection problem. If your Countly Server is down, queued requests can reach excessive numbers, causing delivery problems to the server and requests stored on the device. To prevent this from happening, the Countly iOS SDK will only store requests up to the storedRequestsLimit
. If the number of stored requests reaches the storedRequestsLimit
, the Countly iOS SDK will start to drop the oldest requests, storing the newest ones in their place. If the storedRequestsLimit
is not explicitly set, the default setting will be at 1,000.
config.storedRequestsLimit = 5000;
config.storedRequestsLimit = 5000
Maximum Key Length
You can specify the maxKeyLength
on the CountlyConfig
object before starting Countly.
It is used to limit the length of keys for event names, view names, APM network trace names, APM custom trace names, APM custom trace metric keys, segmentation keys, custom metric keys and custom user property keys.
Keys longer than this limit will be truncated. If not set, it will be 128 chars by default.
config.maxKeyLength = 24;
config.maxKeyLength = 24
Maximum Value Length
You can specify the maxValueLength
on the CountlyConfig
object before starting Countly.
It is used to limit the length of values for segmentation values, APM custom trace metric values, custom crash logs, custom metric values and custom user property values.
Values longer than this limit will be truncated. If not set, it will be 256 chars by default.
config.maxValueLength = 24;
config.maxValueLength = 24
Maximum Segmentation Values
You can specify the maxSegmentationValues
on the CountlyConfig
object before starting Countly.
It is used to limit the number of key-value pairs in segmentations. If there are more key-value pairs than this limit, some of them will be removed. As obviously there is no order among the keys of an NSDictionary, it is not defined which ones will be removed. If not set, it will be 30 by default.
config.maxSegmentationValues = 10;
config.maxSegmentationValues = 10
Always using the POST method
You can set the alwaysUsePOST
flag on theCountlyConfig
object before starting Countly. This flag is used for sending all requests using the HTTP POST method, regardless of their data size. If set, all requests will be sent using the HTTP POST method. Otherwise, only the requests with a file upload or data size of more than 2,048 bytes will be sent using the HTTP POST method.
config.alwaysUsePOST = YES;
config.alwaysUsePOST = true
Custom URLSessionConfiguration
For additional networking settings, you can optionally set a custom URLSessionConfiguration
on the CountlyConfig
object, to be used with all requests sent to Countly Server.
If URLSessionConfiguration
is not set, NSURLSessionConfiguration
's defaultSessionConfiguration
will be used by default.
config.URLSessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
config.URLSessionConfiguration = URLSessionConfiguration.ephemeral
In addition to the initial configuration, you can also change URLSessionConfiguration later using:
[Countly.sharedInstance setNewURLSessionConfiguration:newURLSessionConfiguration];
Countly.sharedInstance().setNewURLSessionConfiguration(newURLSessionConfiguration)
Custom Metrics
For overriding default metrics or adding extra ones that are sent with begin_session
requests, you can use customMetrics
dictionary on the CountlyConfig
object.
Custom metrics should be an NSDictionary
, with keys and values are both NSString
's only.
config.customMetrics = @{@"key": @"value"};
config.customMetrics = ["key": "value"];
config.customMetrics = @{CLYMetricKeyAppVersion: @"1.2.3"};
config.customMetrics = [CLYMetricKeyAppVersion: "1.2.3"];
Attribution
Direct Attribution
You can record direct attribution with campaign type and data.
Currently supported campaign types are countly
and _special_test
.
Campaign data has to be JSON string in {"cid":"CAMPAIGN_ID", "cuid":"CAMPAIGN_USER_ID"}
format.
Calls to this method will be ignored if consent for CLYConsentAttribution
is not given, while requiresConsent
flag is set on initial configuration.
NSString* campaignData = @"{\"keyA\":\"valueA\",\"keyB\":\"valueB\"}";
[Countly.sharedInstance recordDirectAttributionWithCampaignType:@"countly" andCampaignData:campaignData];
let campaignData = "{\"keyA\":\"valueA\",\"keyB\":\"valueB\"}"
Countly.sharedInstance().recordDirectAttribution(withCampaignType: "countly", andCampaignData: campaignData)
Indirect Attribution
You can record indirect attribution with given key-value pairs.
Keys could be a predefined CLYAttributionKeyIDFA
or CLYAttributionKeyADID
or any non-zero length valid string.
Calls to this method will be ignored if consent for CLYConsentAttribution
is not given, while requiresConsent
flag is set on initial configuration.
[Countly.sharedInstance recordIndirectAttribution:@{CLYAttributionKeyADID: @"value", @"key1": @"value1", @"key2": @"value2"}];
Countly.sharedInstance().recordIndirectAttribution([CLYAttributionKey.ADID.rawValue: "value", "key1": "value1", "key2": "value2"])
For obtaining IDFA please see: https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier?language=objc
And for App Tracking Transparency permission required on iOS 14.5+ please see: https://developer.apple.com/documentation/apptrackingtransparency?language=objc
Direct Request
The addDirectRequest
method allows you to send custom key/value pairs directly to your Countly server. This method accepts a dictionary with string key/value pairs as its parameter. In order to use this method effectively, you will need to convert any data that you wish to send (such as dictionaries or arrays) into a URL encoded string before passing it to the method.
Here is a detailed example usage of addDirectRequest
:
- (void) sendDirectRequest {
NSMutableDictionary *requestMap = [[NSMutableDictionary alloc] init];
requestMap[@"city"] = @"Istanbul";
requestMap[@"country_code"] = @"TR";
requestMap[@"ip_address"] = @"41.0082,28.9784";
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
event[@"key"] = @"test";
event[@"count"] = @"201";
event[@"sum"] = @"2010";
event[@"dur"] = @"2010";
NSMutableDictionary *ffJson = [[NSMutableDictionary alloc] init];
ffJson[@"type"] = @"FF";
ffJson[@"start_time"] = [NSNumber numberWithInt:123456789];
ffJson[@"end_time"] = [NSNumber numberWithInt:123456789];
NSMutableDictionary *skipJson = [[NSMutableDictionary alloc] init];
skipJson[@"type"] = @"skip";
skipJson[@"start_time"] = [NSNumber numberWithInt:123456789];
skipJson[@"end_time"] = [NSNumber numberWithInt:123456789];
NSMutableDictionary *resumeJson = [[NSMutableDictionary alloc] init];
resumeJson[@"type"] = @"resume_play";
resumeJson[@"start_time"] = [NSNumber numberWithInt:123456789];
resumeJson[@"end_time"] = [NSNumber numberWithInt:123456789];
NSMutableArray *trickPlay = [[NSMutableArray alloc] init];
[trickPlay addObject:ffJson];
[trickPlay addObject:skipJson];
[trickPlay addObject:resumeJson];
NSMutableDictionary *segmentation = [[NSMutableDictionary alloc] init];
segmentation[@"trickplay"] = trickPlay;
event[@"segmentation"] = segmentation;
NSMutableArray *events = [[NSMutableArray alloc] init];
[events addObject:event];
NSString *eventsString = [self toString:events];
requestMap[@"events"] = eventsString.cly_URLEscaped;
[Countly.sharedInstance addDirectRequest:requestMap];
}
- (NSString *) toString: (id) dictionaryOrArrayToOutput {
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionaryOrArrayToOutput
options:0
error:&error];
NSString *jsonString = @"";
if (! jsonData) {
NSLog(@"Got an error: %@", error);
} else {
jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
return jsonString;
}
watchOS Integration
Just like iPhones and iPads, collecting and analyzing usage statistics and analytics data from an Apple Watch is the key for offering a better experience. Fortunately, the Countly iOS SDK has watchOS support. Here you can find out how to use the Countly iOS SDK in your watchOS apps:
1. First, open a new or your existing Xcode project and add a new target by clicking the + icon at the bottom of the Projects and Targets List. (You can skip to the step 4 if your project already has a Watch App, or you can visit https://apple.co/1PnD1uT for more information)
2. Select the target template WatchKit App under the watchOS > Application section. Do not choose the WatchKit App for watchOS 1. In order to keep things simple, do not include the Notification scene, Glance scene, or Complication.
3. If Xcode asks whether you would like to activate the WatchKit App scheme, click Activate.
4. Now it is time to add the Countly iOS SDK to your project. After cloning the Countly iOS SDK anywhere you would like, Drag&Drop .h
and .m
files in countly-sdk-ios
folder into your Xcode project, and in the following dialog, please ensure the iPhone app target and WatchKit Extension target (not WatchKit App) have been selected as well as the Copy items if needed checkbox.
5. Import Countly.h
into ExtensionDelegate.m
#import "Countly.h"
// No need to import files for Swift projects
6. Add the Countly starting code into the applicationDidFinishLaunching
method ofExtensionDelegate.m
CountlyConfig* config = CountlyConfig.new;
config.appKey = @"YOUR_APP_KEY";
config.host = @"https://YOUR_COUNTLY_SERVER";
[Countly.sharedInstance startWithConfig:config];
let config: CountlyConfig = CountlyConfig()
config.appKey = "YOUR_APP_KEY"
config.host = "https://YOUR_COUNTLY_SERVER"
Countly.sharedInstance().start(with: config)
7. Add suspend code into the applicationWillResignActive
method of ExtensionDelegate.m
[Countly.sharedInstance suspend];
Countly.sharedInstance().suspend()
8. Add resume code into the applicationDidBecomeActive
method of ExtensionDelegate.m
[Countly.sharedInstance resume];
Countly.sharedInstance().resume()
9. After adding these three lines of code into the related extension delegate methods, try building the project. Everything should be OK now. If you run the watchOS app, you can see the session on your Countly dashboard.
Now you are ready to track your watchOS app with Countly.
By the way, the session concept on watchOS is slightly different than the one on the iOS, as watchOS apps are intended for brief user interaction. So, there are two values you might need to adjust depending on your watch apps’ use cases.
-
The first value is
updateSessionPeriod
. Its default value is 20 seconds for watchOS and 60 seconds for iOS. This value determines how often session updating requests will be sent to the server while the app is in use. -
The second value is
eventSendThreshold
, which is 3 for watchOS and 10 for iOS by default. The Countly iOS SDK waits for the number of recorded unique events to reach this threshold to deliver them to the server until the next session updating kicks in. Considering the fact that Apple Watch is designed to be used for short sessions, these values generally seem appropriate. However, you can change them depending on your watchOS app’s scenario.
config.updateSessionPeriod = 15;
config.eventSendThreshold = 1;
config.updateSessionPeriod = 15
config.eventSendThreshold = 1
Automatic Reference Counting (ARC)
The Countly iOS SDK uses Automatic Reference Counting (ARC). If you are integrating the Countly iOS SDK into a non-ARC project, you should add the-fobjc-arc
compiler flag to all Countly iOS SDK implementation (*.m
) files found under Target
> Build Phases
> Compile Sources
.
App Transport Security (ATS)
With App Transport Security introduced in iOS 9, connections to non-HTTPS servers which does not meet some requirements will fail with the following error: Error: Error Domain=NSURLErrorDomain Code=-1022 "The resource could not be loaded because the App Transport Security policy requires the use of a secure connection."
. You can see details of the requirements here. If your Countly Server instance does not meet these requirements, you can need to add the NSAppTransportSecurity
key into your targets' Info.plist
files, with NSAllowsArbitraryLoads
or NSExceptionDomains
as the value, to communicate with your Countly Server.
Swift Projects
For using Countly on Swift based projects, please ensure your Bridging Header File is configured properly for each target. Then import the Countly.h
file into the Bridging Header file, after which you can seamlessly use the Countly methods in your Swift projects.
For Notification Service Extension targets, import CountlyNotificationService.h
into the Bridging Header file.
You can view more details on how to create a Bridging Header file here.
Updating the Countly iOS SDK
Before upgrading to a new version of the Countly iOS SDK, do not forget to remove the existing files from your project first. Then, while adding the new Countly iOS SDK files again, please ensure you have chosen the targets correctly and select the "Copy items if needed" checkbox.
CocoaPods
While the Countly iOS SDK supports integration via CocoaPods, we can not be able to help you with issues stemming from the CocoaPods itself, especially for some advanced use-cases.
You can integrate the Countly iOS SDK using CocoaPods. For more information, please see the Countly CocoaPods page. Please ensure you have the latest version of CocoaPods and your local spec repo is updated. For Notification Service Extension targets, please ensure your Podfile uses something similar to the following sub specs:
target 'MyMainApp' do
platform :ios,'10.0'
pod 'Countly'
end
target 'CountlyNSE' do
platform :ios,'10.0'
pod 'Countly/NotificationService'
end
For Swift projects with Notification Service Extension, please see: https://github.com/Countly/countly-sample-ios/issues/13#issuecomment-408652426
For Crash Reporting automatic dSYM uploading, please manually add the dSYM uploader script to your project.
Carthage
You can integrate the Countly iOS SDK using Carthage, just add the following to your project's Cartfile:
github "Countly/countly-sdk-ios"
Swift Package Manager (SPM)
You can integrate the Countly iOS SDK with Swift Package Manager (SPM) using https://github.com/Countly/countly-sdk-ios.git repository URL. Open your XCode and go to File > Add Packages and enter the URL into the search bar. From here you can add the package by targeting the master branch.
Server Configuration
This is an experimental feature!
You can make your SDK fetch some configurations you have set in your Countly server by setting enableServerConfiguration
to true during init:
config.enableServerConfiguration = YES;
config.enableServerConfiguration = true
A/B Testing Variant Information
You can access all the A/B test variants for your Countly application within your mobile app. This information can be useful while testing your app with different variants. There are four calls you can use for fetching, accessing, and enrolling for your variants.
Fetching Test Variants
You can fetch a map of all A/B testing parameters (keys) and variants associated with it:
[Countly.sharedInstance.remoteConfig testingDownloadVariantInformation:^(CLYRequestResult _Nonnull response, NSError *_Nonnull error) {
// do sth
}];
Countly.sharedInstance().remoteConfig.testingDownloadVariantInformation({ response, error in
// do something
})
You must provide a callback to be called when the fetching process ends. Depending on the situation, this would return a RequestResponse Enum (Success, NetworkIssue, or Error) as the first parameter and a String error as the second parameter if there was an error ("null" otherwise).
Accessing Fetched Test Variants
When test variants are fetched, they are stored in memory. As it is not stored persistently if the memory is erased (like in the case of an app restart), you must fetch the variants again. So a common flow is to use the fetched values right after fetching them. To access all fetched values, you can use:
NSDictionary*allVariants = [Countly.sharedInstance.remoteConfig testingGetAllVariants];
let allVariants = Countly.sharedInstance().remoteConfig.testingGetAllVariants()
This would return a dictionary where a test's parameter is associated with all variants under that parameter. The parameter would be the key, and its value would be a String Array of variants. For example:
{
"key_1" : ["variant_1", "variant_2"],
"key_2" : ["variant_3"]
}
Or instead you can get the variants of a specific key:
NSArray* variants = [Countly.sharedInstance.remoteConfig testingGetVariantsForKey:key];
let variants = Countly.sharedInstance().remoteConfig.testingGetVariants(for: key) as NSArray
This would only return a String array of variants for that specific key. If no variants were present for a key, it would return an empty array. A typical result would look like this:
["variant_1", "variant_2"]
Enrolling For a Variant
After fetching A/B testing parameters and variants from your server, you next would like to enroll the user to a specific variant. To do this, you can use the following method:
[Countly.sharedInstance.remoteConfig testingEnrollIntoVariant:key variantName:variantName completionHandler:^(CLYRequestResult _Nonnull response, NSError *_Nonnull error) {
// do sth
}];
Countly.sharedInstance().remoteConfig.testingEnroll(intoVariant: key, variantName: variantName) { response, error in
// do sth
}
Here the 'valueKey' would be the parameter of your A/B test, and 'variantName' is the variant you have fetched and selected to enroll for. The callback function is mandatory and works the same way as explained above in the Fetching Test Variants section.
Drop Old Requests
If you are concerned about your app being used sparsely over a long time frame, old requests inside the request queue might not be important. If, for any reason, you don't want to get data older than a certain timeframe, you can configure the SDK to drop old requests:
config.setRequestDropAgeHours(10);
config.setRequestDropAgeHours(10)
By using the setRequestDropAgeHours
method while configuring the SDK initialization options, you can set a timeframe (in hours) after which the requests would be removed from the request queue. For example, by setting this option to 10, the SDK would ensure that no request older than 10 hours would be sent to the server.
FAQ
This section highlights the most frequently asked questions and any troubleshooting queries you may face while integrating the Countly iOS SDK into your iOS, watchOS, tvOS, or macOS applications.
What platforms does Countly iOS SDK support?
Even though its official name is Countly iOS SDK, it supports all Apple platforms (macOS, tvOS, and watchOS), in addition to iOS. You can use the same SDK for all kinds of projects with different sets of features available for each platform. You can also see how to integrate it into your projects by checking our sample apps here.
Which features are available for each platform?
In addition to Analytics, Events, and User Profiles features, Countly iOS SDK has Push Notifications, Crash Reporting, Auto View Tracking, Remote Config, and Star-Rating features. Availability of these features for platforms are as follows:
-
iOS
Analytics
,Custom Events
,User Profiles
,Push Notifications
,Crash Reporting
,Auto View Tracking
,Star-Rating
,Remote Config
, -
macOS
Analytics
,Custom Events
,User Profiles
,Push Notifications
,Crash Reporting
,Remote Config
, -
tvOS
Analytics
,Custom Events
,User Profiles
,Auto View Tracking
,Crash Reporting
,Remote Config
, -
watchOS
Analytics
,Custom Events
,User Profiles
,Crash Reporting
,Remote Config
,
Can I integrate Countly iOS SDK using CocoaPods?
We keep our Countly.podspec
file up-to-date, so you can integrate Countly iOS SDK using CocoaPods. But, please make sure you read our notes to avoid issues.
How can I tell which Countly iOS SDK version I am using?
You can check for kCountlySDKVersion
constant in Countly iOS SDK source. It is defined as NSString* const kCountlySDKVersion = @"18.08";
What is the difference between Default properties and Custom properties of User Profiles?
User Profiles (only available in Enterprise Edition) has two kinds of properties: Default properties and Custom properties.
Default properties are predefined fields like name
, username
, email
, birth year
, organization
, gender
, phone number
and profile picture
. They are displayed in their own place in User Profiles section. You can set them using default properties on Countly.user
singleton ( Ex: Countly.user.email = @"john@doe.com";
) and record them using [Countly.user save];
method.
Custom properties are custom defined key-value pairs. You can set them using Countly.user.custom
dictionary ( Ex: Countly.user.custom = @{@"testkey1":@"testvalue1", @"testkey2":@"testvalue2"};
) and record them using [Countly.user save];
method as well.
In addition to this, you can use Custom Property Modifiers to set, unset or modify Custom Properties and record your changes using [Countly.user save];
method again.
For details please see User Profiles documentation.
How can I handle logged in and logged out users?
When a user logs in on your app and you have a uniquely identifiable string for that user (like user ID or email address), you can use it instead of device ID to track all info afterwards, without losing all the data generated by that user so far. You can use changeDeviceIDWithMerge:
method ( Ex: [Countly.sharedInstance changeDeviceIDWithMerge:@"user123@example.com"];
). This will replace previously used device ID on device, and merge all existing data on server.
Why are events not displayed on Countly Server dashboard?
Events are queued but not sent to server until next updateSessionPeriod
(60 seconds by default) or eventSendThreshold
(10 by default) is reached. So, a little delay may be expecting in displaying events on Countly Server dashboard, while still seeing session data immediately.
In addition to this, Countly iOS SDK sends previously stored requests, if any, followed by a begin_session
request, when it starts. If your app records any events meanwhile, these events will be queued and sent to server when all previously queued requests are successfully completed.
Is it possible to use Countly iOS SDK with another crash SDK?
In iOS there can only be one uncaught exception handler. Even though it is possible to save the previous handler and pass the uncaught exception to the previous handler as well, it is not safe to assume that it will work in all cases. We do not know how other SDKs are implemented or whether iOS will give enough time for the all the handlers to do their work before terminating the app, hence, we advise to use Countly as the only crash handler.
Why are my test crashes not reported?
If you are running your app with Xcode debugger attached while forcing a test crash, Countly iOS SDK cannot handle the crash as debugger will be intercepting. Please make sure you run your app without Xcode debugger attached.
How can I manually record push notification custom button actions?
If you have set doNotShowAlertForNotifications
flag on initial configuration object to handle push notifications manually, you can create your own custom UI to show notification message and action buttons. For this, just implement - (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
method in your application's delegate. For details of handling notification manually, please see Handling Notifications Manually section.
How can I get rid of compiler warning "No rule to process file"?
If you get Warning: no rule to process file '../countly-sdk-ios/README.md' of type net.daringfireball.markdown for architecture arm64
in Xcode, it means README.md
(and/or CHANGELOG.md
) file is added to Build Phases > Compile Sources
in your target. Please remove README.md
from Compile Sources
list.
How is Countly affected by Apple's App Tracking Transparency changes?
As Countly is not and has never been doing any tracking, it is not affected by Apple's App Tracking Transparency changes.
Definition of "tracking" by Apple's User Privacy and Data Use guidelines:
“Tracking” refers to linking data collected from your app about a particular end-user or device, such as a user ID, device ID, or profile, with Third-Party Data for targeted advertising or advertising measurement purposes, or sharing data collected from your app about a particular end-user or device with a data broker.
For further information please see App Privact Details section on Apple Developer website.
What is the average data size of a Countly iOS SDK request sent to Countly Server?
While there are several types of requests that Countly iOS SDK sends to Countly Server, the most common ones are:
-
Begin Session Request: It is sent on every app launch (and session start after coming back from the background), and it includes basic metrics.
An example Begin Session request (498 bytes
) :
http://mycountlyserver.com/i?app_key=0000000000000000000000000000000000000000
&device_id=00000000-0000-0000-0000-000000000000
×tamp=1534402860000&hour=16&dow=5&tz=540
&sdk_version=18.08&sdk_name=objc-native-ios
&begin_session=1
&metrics=%7B%22_device%22%3A%22iPhone9%2C1%22%2C%22_os%22%3A%22iOS%22%2C%22_os_version%22%3A%2211.4.1%22%2C%22_locale%22%3A%22en_JP%22%2C%22_density%22%3A%22%402x%22%2C%22_resolution%22%3A%22750x1334%22%2C%22_app_version%22%3A%221.0%22%2C%20%22_carrier%22%3A%22NTT%22%7D
-
Update Session Request: It is sent every 60 seconds by default, but it depends on Countly iOS SDK initial configuration.
An example Update Session request (233 bytes
) :
http://mycountlyserver.com/i?app_key=0000000000000000000000000000000000000000
&device_id=00000000-0000-0000-0000-000000000000
×tamp=1534402920000&hour=16&dow=5&tz=540
&sdk_version=18.08&sdk_name=objc-native-ios
&session_duration=60
-
End Session Request: It is sent at the end of a session, when the app goes to background or terminates.
An example End Session request (247 bytes
) :
http://mycountlyserver.com/i?app_key=0000000000000000000000000000000000000000
&device_id=00000000-0000-0000-0000-000000000000
×tamp=1534402956000&hour=16&dow=5&tz=540
&sdk_version=18.08&sdk_name=objc-native-ios
&session_duration=36
&end_session=1
- Other Requests For Events, User Details, Push Notifications, Crash Reporting, View Tracking, Feedbacks, Consents, and some other features: Countly iOS SDK sends various requests with various data sizes. Frequency and size of these requests depend on Countly iOS SDK initial configuration and your app's use cases, as well as the end user.
What data metrics are collected by Countly iOS SDK?
Countly iOS SDK collects following metrics by default:
- Device Model
- Screen Resolution
- Screen Density
- OS Name
- OS Version
- App Version
- Locale Identifier
- Carrier
Further, if Apple Watch feature is enabled: - Paired Apple Watch Presence - watchOS App Install Status
Why are push notification action events not reported?
Notification action event is recorded when a user interacts with the notification and system calls didReceiveNotificationResponse:
method.
If you tap on the notification, but still do not get any action events, please check for the following possible problem points:
- You are setting
UNUserNotificationCenter.currentNotificationCenter
's delegate manually at some point, so Countly iOS SDK can not handle the notification. -
requiresConsent
Flag is enabled on initial config, but consent for Push Notifications feature is not granted (Note that this has nothing to do with iOS notification permission). - Notification is not coming from Countly and it does not have any value for
kCountlyPNKeyNotificationID = @"i"
key in it