This guide provides detailed information about our iOS SDK. All of our SDKs are open source. Go to our iOS SDK GitHub repository to see the source code.
Language Support
This library is compatible with iOS deployment target versions 9.0 and later, and Swift version 4 and later.
Initialization
To get started, set up Split in your code base with two simple steps.
1. Import the SDK into your project
Swift Package Manager
You can import the SDK in your project by using Swift Package Manager. This can be done through XCode or by editing manually the Package.swift file to add the iOS SDK repository as a dependency.
CocoaPods
You can also import the SDK into your Xcode project using CocoaPods, adding it in your Podfile.
pod 'Split', '~> 2.10.0'
Carthage
This is another option to import the SDK. Just add it in your Cartfile.
github "splitio/ios-client" 2.10.0
Then follow the steps provided in the Carthage Readme.
2. Instantiate the SDK and create a new Split client
The first time that the SDK is instantiated, it kicks off background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Split servers. This process can take up to a few hundred milliseconds depending on the size of the data.
If the SDK is asked to evaluate which treatment to show to a customer for a specific split while it is in this intermediate state, it may not have the data necessary to run the evaluation. In this case, the SDK does not fail, rather, it returns the control treatment.
After the first initialization, the fetched data is stored. Further initializations fetch data from that cache and the configuration is available immediately.
We recommend instantiating the SDK once as a singleton and reusing it throughout your application.
Include the API key for the environment that you are setting up the SDK for. The API key is available on your Organization Settings page, on the APIs tab. Choose the "browser" type. This is a special type of API token with limited privileges to be used for the untrustable world of browsers or mobile clients. Learn more about API keys.
import Split
// Your Split API-KEY
let apiKey: String = "YOUR_API_KEY"
//User Key
let key: Key = Key(matchingKey: "key")
//Split Configuration
let config = SplitClientConfig()
//Split Factory
let builder = DefaultSplitFactoryBuilder()
let factory = builder.setApiKey(apiKey).setKey(key).setConfig(config).build()
//Split Client
let client = factory?.client
Using the SDK
Basic use
To make sure the SDK is properly loaded before asking it for a treatment you should wait until the SDK is ready as shown below. We set the client to listen for the sdkReady
event triggered by the SDK before asking for an evaluation.
Once the sdkReady
event fires, you can use the getTreatment method to return the proper treatment based on the SPLIT_NAME you pass and the key you passed when instantiating the SDK.
From there, you simply need to use an if-else-if block as shown below and plug the code in for the different treatments that you defined in the Split UI. Make sure to remember the final else branch in your code to handle the client returning control.
client?.on(event: SplitEvent.sdkReady) {
// Evaluate key in Split
let treatment = client?.getTreatment("SPLIT_NAME")
if treatment == "on" {
// insert code here to show on treatment
} else if treatment == "off" {
// insert code here to show off treatment
} else {
// insert your control treatment code here
}
}
client?.on(event: SplitEvent.sdkReadyTimedOut) {
//handle for timeouts here
print("SDK time out")
}
Also, a sdkReadyFromCache
event is available, which allows to be aware of when the SDK has loaded data from cache. This way it is ready to evaluate splits using those locally cached definitions.
client?.on(event: SplitEvent.sdkReadyFromCache) {
// Evaluate key in Split
let treatment = client?.getTreatment("SPLIT_NAME")
if treatment == "on" {
// insert code here to show on treatment
} else if treatment == "off" {
// insert code here to show off treatment
} else {
// insert your control treatment code here
}
}
Shutdown
Before letting your app shut down, call destroy()
as it gracefully shuts down the Split SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events.
client?.destroy()
Also, this method has a completion closure which can be used to run some code after destroy was executed.
For instance, the following snippet waits until destroy has finished to continue execution:
let semaphore = DispatchSemaphore(value: 0)
client?.destroy(completion: {
_ = semaphore.signal()
})
semaphore.wait()
After destroy()
has been called, any subsequent invocations to the client.getTreatment()
or manager
methods result in control
or empty list respectively.
Important!
A call to the destroy()
method also destroys the factory object. When creating new client instance, first create a new factory instance.
Multiple evaluations at once
In some instances, you may want to evaluate treatments for multiple splits at once. Use the GetTreatments
method of the Split client to do this. Simply pass a list of the split names you want treatments for and the method returns an object with the results.
let splitList = ["SPLIT_NAME_1", "SPLIT_NAME_2", "SPLIT_NAME_3"]
let treatments = client?.getTreatments(splits: splitList, attributes: nil)
// treatments will have the following form:
// {
// "SPLIT_NAME_1": "result_1",
// "SPLIT_NAME_2": "result_2",
// "SPLIT_NAME_3": "result_3"
// }
Attribute syntax
To target based on custom attributes, the SDK's getTreatment
method needs to be passed an attribute map at runtime.
In the example below, we are rolling out a split to users. The provided attributes plan_type
, registered_date
, permissions
, paying_customer
, and deal_size
are passed to the getTreatment
call. These attributes are compared and evaluated against the attributes used in the Rollout plan as defined in the Split Web console to decide whether to show the on
or off
treatment to this account.
The getTreatment
method supports five types of attributes: strings, numbers, dates, booleans, and sets. The proper data type and syntax for each are:
- Strings: Use type String.
- Numbers: Use type
Int64
. - Dates: Use the value
TimeInterval
. For instance, the value for theregistered_date
attribute below isDate().timeIntervalSince1970
, which is aTimeInterval
value. - Booleans: Use type
Bool
. - Sets: Use type
[String]
.
var attributes: [String:Any] = [:]
attributes["plan_type"] = "growth"
attributes["registered_date"] = Date().timeIntervalSince1970
attributes["deal_size"] = 1000
attributes["paying_customer"] = true
let perms: [String] = ["read", "write"];
attributes["permissions"] = perms
// See client initialization above
let treatment = client?.getTreatment("SPLIT_NAME", attributes: attributes)
if treatment == "on" {
// insert code here to show on treatment
} else if treatment == "off" {
// insert code here to show off treatment
} else {
// insert your control treatment code here
}
Get Treatments with Configurations
To leverage dynamic configurations with your treatments, you should use the getTreatmentWithConfig
method.
This method will return an object containing the treatment and associated configuration.
The config element will be a stringified version of the configuration JSON defined in the Split web console. If there is no configuration defined for a treatment, the SDK will return null
for the config parameter.
This method takes the exact same set of arguments as the standard getTreatment
method. See below for examples on proper usage:
let result = client.getTreatmentWithConfig("new_boxes", attributes: attributes)
let config = try? JSONSerialization.jsonObject(with: result.config.data(using: .utf8)!, options: []) as? [String: Any]
let treatment = result.treatment
If you need to get multiple evaluations at once, you can also use the get_treatments_with_config
. The method takes the exact same arguments as the get_treatments
method but just returns a mapping of split names to splitResults instead of strings. Example usage below.
let splitList = ["SPLIT_NAME_1", "SPLIT_NAME_2", "SPLIT_NAME_3"]
let treatments = client?.getTreatmentsWithConfig(splits: splitList, attributes: nil)
// treatments will have the following form:
// {
// "SPLIT_NAME_1": { "treatment": "on", "config": "{ \"color\":\"red\" }"},
// "SPLIT_NAME_2": { "treatment": "visa", "config": "{ \"color\":\"red\" }"}
// }
Track
Use the track
method to record any actions your customers perform. Each action is known as an event
and corresponds to an event type
. Calling track
through one of our SDKs or via the API is the first step to getting experimentation data into Split and allows you to measure the impact of your splits on your users' actions and metrics.
Learn more about using track events in splits.
In the examples below you can see that the .track()
method can take up to four arguments. The proper data type and syntax for each are:
- TRAFFIC_TYPE: The traffic type of the key in the track call. The expected data type is String. You can only pass values that match the names of traffic types that you have defined in your instance of Split.
- EVENT_TYPE: The event type that this event should correspond to. The expected data type is String. Full requirements on this argument are:
- Contains 63 characters or fewer.
- Starts with a letter or number.
- Contains only letters, numbers, hyphen, underscore, or period.
- This is the regular expression we use to validate the value:
[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}
- VALUE: (Optional) The value to be used in creating the metric. This field can be sent in as null or 0 if you intend to purely use the count function when creating a metric. The expected data type is Double.
- PROPERTIES: (Optional) An object of key value pairs that can be used to filter your metrics. Learn more about event property capture here. Split currently supports three types of properties: strings, numbers, and booleans.
The track
method returns a boolean value of true
or false
to indicate whether or not the SDK was able to successfully queue the event to be sent back to Split's servers on the next event post. The SDK will return false
if the current queue size is equal to the config set by eventsQueueSize
or if an incorrect input to the track
method has been provided.
In the case that a bad input has been provided, you can read more about our SDK's expected behavior here
// If you would like to send an event without a value
let resp = client?.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE")
// Example
let resp = client?.track(trafficType: "user", eventType: "page_load_time")
// If you would like to associate a value to an event
let resp = client?.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE", value: VALUE)
// Example
let resp = client?.track(trafficType: "user", eventType: "page_load_time", value: 83.334)
// If you would like to associate just properties to an event
let resp = client?.track(trafficType: "TRAFFIC_TYPE", eventType: "EVENT-TYPE", properties: PROPERTIES)
// Example
let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true]
let resp = client?.track(trafficType: "user", eventType: "page_load_time", properties: properties)
// If you would like to send an event but you've already defined the traffic type in the config of the client
let resp = client?.track(eventType: "EVENT-TYPE")
// Example
let resp = client?.track(eventType: "page_load_time")
// If you would like to associate a value to an event and you've already defined the traffic type in the config of the client
let resp = client.track(eventType: "EVENT-TYPE", value: VALUE)
// Example
let resp = client?.track(eventType: "page_load_time", value: 83.334)
// If you would like to associate properties to an event and you've already defined the traffic type in the config of the client
let resp = client.track(eventType: "EVENT-TYPE", properties: PROPERTIES)
// Example
let properties: [String:Any] = ["package": "premium", "discount": 50, "admin": true]
let resp = client?.track(eventType: "page_load_time", proerties: properties)
Manager
Use the Split Manager to get a list of features available to the Split client.
To instantiate a Manager in your code base, use the same factory that you used for your client.
let apiKey: String = "YOUR_API_KEY"
let key: Key = Key(matchingKey: "key")
let config = SplitClientConfig()
let builder = DefaultSplitFactoryBuilder()
let factory =
builder.setApiKey(apiKey).setKey(key).setConfig(config).build()
let manager = factory?.manager
The Manager then has the following properties and methods available.
/**
* Retrieves the Splits that are currently registered with the
* SDK.
*
* @return an array of SplitView or empty.
*/
var splits: [SplitView] { get }
/**
* Returns the names of features (or Splits) registered with the SDK.
*
* @return an array of String (Split Feature Names) or empty
*/
var splitNames: [String] { get }
/**
* Returns the Splits registered with the SDK of this name.
*
* @return SplitView or nil
*/
func split(featureName: String) -> SplitView?
The SplitView
class referenced above has the following structure.
@objc public class SplitView: NSObject, Codable {
public var name: String?
public var trafficType: String?
public var killed: Bool?
public var treatments: [String]?
public var changeNumber: Int64?
}
Configuration
The SDK has a number of knobs for configuring performance. Each knob is tuned to a reasonable default. However, you can override the value while instantiating the SDK. The parameters available for configuration are shown below.
Configuration | Description | Default value |
---|---|---|
featuresRefreshRate | The SDK polls Split servers for changes to feature splits at this rate (in seconds). | 3600 seconds (1 hour) |
segmentsRefreshRate | The SDK polls Split servers for changes to segments at this rate (in seconds). | 1800 seconds (30 minutes) |
impressionRefreshRate | The treatment log captures which customer saw what treatment (on, off, etc.) at what time. This log is periodically flushed back to Split servers. This configuration controls how quickly the cache expires after a write (in seconds). | 1800 seconds (30 minutes) |
impressionsQueueSize | Default queue size for impressions. | 30K |
eventsPushRate | When using .track , how often the events queue is flushed to Split servers. |
1800 seconds |
eventsPerPush | Maximum size of the batch to push events. | 2000 |
eventsFirstPushWindow | Amount of time to wait for the first flush. | 10 seconds |
eventsQueueSize | When using .track , the number of events to be kept in memory. |
10000 |
trafficType | - | not set |
isDebugModeEnabled | Enabled debug mode. | false |
isVerboseModeEnabled | Enabled verbose mode. | false |
streamingEnabled | Boolean flag to enable the streaming service as default synchronization mechanism when in foreground. In the event of an issue with streaming, the SDK will fallback to the polling mechanism. If false, the SDK will poll for changes as usual without attempting to use streaming. | true |
sync | Optional SyncConfig instance. Use it to filter specific splits to be synced and evaluated by the SDK. These filters can be created with the SplitFilter::byName static function by passing a list of split names as string values and appended to this config using the SyncConfig builder. If not set or empty, all splits will be downloaded by the SDK. |
null |
To set each of the parameters defined above, use the syntax below.
import Split
// Your Split API-KEY
let apiKey: String = "YOUR_API_KEY"
//User Key
let key: Key = Key(matchingKey: "key")
//Split Configuration
let config = SplitClientConfig()
config.impressionRefreshRate = 30
config.isDebugModeEnabled = false
let syncConfig = SyncConfig.builder()
.addSplitFilter(SplitFilter.byName(["SPLIT_NAME_1", "SPLIT_NAME_2"]))
.build()
config.sync = syncConfig
//Split Factory
let builder = DefaultSplitFactoryBuilder()
let factory =
builder.setApiKey(apiKey).setKey(key).setConfig(config).build()
//Split Client
let client = factory?.client
Localhost Mode
Features start their life on one developer's machine. A developer should be able to put code behind Splits on their development machine without the SDK requiring network connectivity. To achieve this, the Split SDK can be started in 'localhost' mode. In this mode, the SDK neither polls nor updates Split servers, rather it uses an in-memory data structure to determine what treatments to show to the customer for each of the features.
Here is how you can start the SDK in 'localhost' mode:
Since version 2.1.0, our SDK supports a new type of localhost split definition file, using the YAML format.
This new format allows the user to map different keys to different treatments within a single split, and also add configurations to them.
This file has to be included into the project bundle and it will be used as an initial file. It will be copied to the cache folder, then it can be edited while app is running to simulate split changes. When no file is added to the app bundle, an error will occur.
The new format is a list of single-key maps (one per mapping split-keys-config), defined as follows:
# - feature_name:
# treatment: "treatment_applied_to_this_entry"
# keys: "single_key_or_list"
# config: "{\"desc\" : \"this applies only to ON treatment\"}"
- my_feature:
treatment: "on"
keys: "key"
config: "{\"desc\" : \"this applies only to ON treatment\"}"
- some_other_feature:
treatment: "off"
- my_feature:
treatment: "off"
- other_feature:
treatment: "off"
keys: ["key_1", "key_2"]
config: "{\"desc\" : \"this overrides multiple keys and returns off treatment for those keys\"}"
In the example above, we have 3 entries:
- The first one defines that for split
my_feature
, the keykey
will return the treatmenton
and theon
treatment will be tied to the configuration{"desc" : "this applies only to ON treatment"}
. - The second entry defines that the feature
some_other_feature
will always return theoff
treatment and no configuration. - The third entry defines that
my_feature
will always returnoff
for all keys that don't match another entry (in this case, any key other thankey
). - The forth entry shows how an example to override a treatment for a set of keys.
You can set the name of the Split localhost YAML file within cache folder as shown in the example below:
// Split API-KEY must be "localhost"
let apiKey: String = "localhost"
let key: Key = Key(matchingKey: "key")
let config = SplitClientConfig()
config.splitFile = "localhost.yaml"
let builder = DefaultSplitFactoryBuilder()
self.factory =
builder.setApiKey("localhost").setKey(key).setConfig(config).build()
If SplitClientConfig.splitFile is not set, Split SDK maintains backward compatibility by trying to load the legacy file (.splits), now deprecated.
In this mode, the SDK loads a local file called localhost.splits which has the following line format:
SPLIT TREATMENT
Additionally, you can include comments to the file starting a line with the # character
Example: A sample localhost.splits file
# This line is a comment
# Following line has feature = FEATURE_ONE and treatment = ON
FEATURE_ONE ON
FEATURE_TWO OFF
# Previous line has feature = FEATURE_TWO, treatment = OFF
By enabling debug mode, the localhost file location will be logged to the console so that is possible to open it with a text editor when working on the simulator. When using the device to run the app, the file can be modified by overwriting the app's bundle from the Device and Simulators tool.
Impression listener
Split SDKs send impression data back to Split servers periodically and as a result of evaluating splits. To additionally send this information to a location of your choice, define and attach an impression handler.
The SDK sends the generated impressions to the impression handler right away. As a result, be careful while implementing handling logic to avoid blocking the main thread. Generally speaking, you should create a separate thread to handle incoming impressions (see the snippet below).
let config = SplitClientConfig()
config.setImpressionListener() { (impression) in
// Do some work on main thread
DispatchQueue.global().async {
// Do some async work (use this most of the time!)
}
}
let key: Key = Key(matchingKey: "key")
let builder = DefaultSplitFactoryBuilder()
let factory =
builder.setApiKey(apiKey).setKey(key).setConfig(config).build()
let client = factory?.client
Flush
The flush() method sends the data stored in memory (impressions and events) to Split cloud and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt.
client.flush()
Advanced: Subscribe to events
You can listen for three different types of events from the SDK.
sdkReadyFromCache
- The SDK is ready to evaluate using cached data (which might be stale). If conditions are met, this event will be emitted almost immediately since access to the cache is synchronous. Otherwise it won't fire.sdkReady
- The SDK is ready to be used and has the most up-to-date snapshot of your splits.sdkReadyTimedOut
- This event fires if there is no last known configuration in disk cache, and the SDK could not acquire the information from our servers after a given period of time, determined by theready
parameter inSplitClientConfig
.sdkUpdated
- This event fires whenever a split or segment is changed. Use this if you want to reload your app every time you make a change in the Split Web console.
SDK event handling is done through the function on(event:execute:)
, which receives a closure as an event handler.
The code within the closure is executed on the main thread. For that reason, running code in the background must be done explicitly.
The syntax to listen for an event is shown below.
...
let client = factory.client
client.on(event: SplitEvent.sdkReady, execute: {
// The client is ready for start making evaluations with your data
// Do some stuff on main thread
})
// Or
client.on(event: SplitEvent.sdkReadyTimeOut) {
// This callback will be called if and only if the client is configured with ready timeout and
// is not ready for that time or if the API key is wrong.
// You can still call getTreatment() but it could return CONTROL.
// Do some stuff on main thread
}
client.on(event: SplitEvent.sdkReadyFromCache) {
// Fired after the SDK could confirm the presence of the Split data.
// This event fires quickly, since there's no actual fetching of information.
// Keep in mind that data might be stale, this is NOT a replacement of sdkReady.
// Do some stuff on main thread
}
client.on(event: SplitEvent.sdkUpdated) {
// fired each time the client state change.
// For example, when a Split or a Segment changes.
// Do some stuff on main thread
}
...
Comments
0 comments
Please sign in to leave a comment.