For the complete documentation index, see llms.txt. This page is also available as Markdown.

iOS SDK

Integrate the Harness FME iOS SDK to manage feature flags and data-driven experiments in Swift and Objective-C apps.

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.

Before you begin

This library is compatible with iOS and tvOS deployment target versions 9.0+, macOS 10.11+, and watchOS 7.0+. Xcode 12 and later is also required, but we recommend the minimum version necessary to publish apps on the AppStore.

RULE-BASED SEGMENTS SUPPORT

Rule-based segments are supported in SDK versions 3.3.0 and above. No changes are required to your SDK implementation, but updating to a supported version is required to ensure compatibility.

Older SDK versions will return the control treatment for flags using rule-based segments and log an impression with a special label for unsupported targeting rules.

Initialization

To get started, set up FME in your code base with the two following 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', '~> 3.7.1'

Carthage

This is another option to import the SDK. Just add it in your Cartfile.

Once added, follow the steps provided in the Carthage Readme.

2. Instantiate the SDK and create a new SDK factory client

The first time that the SDK is instantiated, it starts background tasks to update an in-memory cache and in-storage cache with small amounts of data fetched from Harness 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 feature flag 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 factory once as a singleton and reusing it throughout your application.

Configure the SDK with the SDK key for the FME environment that you would like to access. In legacy Split (app.split.io) the SDK key is found on your Admin settings page, in the API keys section. Select a client-side SDK API key. This is a special type of API token with limited privileges for use in browsers or mobile clients. See API keys to learn more.

Use the SDK

Basic use

To make sure the SDK is properly loaded before asking it for a treatment, 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 feature flag name you pass and the key you passed when instantiating the SDK.

From there, you need to use an if-else-if block as shown below and plug the code in for the different treatments that you defined in Harness FME. Make sure to remember the final else branch in your code to handle the client returning control.

Also, a sdkReadyFromCache event is available, which allows you to be aware of when the SDK has loaded data from cache. This way it is ready to evaluate feature flags using those locally cached definitions.

Starting from version 2.24.5, it is possible to configure the handler to run in a background thread or specify a custom queue.

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 feature flag 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 Harness FME 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 the registered_date attribute below is Date().timeIntervalSince1970, which is a TimeInterval value.

  • Booleans: Use type Bool.

  • Sets: Use type [String].

Binding attributes to the client

Attributes can be bound to the client at any time during the SDK lifecycle. These attributes will be stored in memory and used in every evaluation to avoid the need for keeping the attribute set accessible through the whole app. These attributes can be cached into the persistent caching mechanism of the SDK making them available for future sessions, as well as part of the SDK_READY_FROM_CACHE flow by setting the persistentAttributesEnabled to true. No need to wait for your attributes to be loaded at every session before evaluating flags that use them.

When an evaluation is called, the attributes provided (if any) at evaluation time are combined with the ones already loaded into the SDK memory, with the ones provided at function execution time take precedence, enabling for those attributes to be overridden or hidden for specific evaluations.

An attribute is considered valid if it follows one of the types listed below:

  • String

  • Number

  • Boolean

  • Array

The SDK validates these before storing them and if there are invalid or missing values, possibly indicating an issue, the methods return the boolean false and do not update any value.

The snippet below shows how to update these attributes:

Multiple evaluations at once

In some instances, you may want to evaluate treatments for multiple feature flags at once. Use the different variations of getTreatments method of the SDK factory client to do this.

  • getTreatments: Pass a list of the feature flag names you want treatments for.

  • getTreatmentsByFlagSet: Evaluate all flags that are part of the provided set name and are cached on the SDK instance.

  • getTreatmentsByFlagSets: Evaluate all flags that are part of the provided set names and are cached on the SDK instance.

Get treatments with configurations

To leverage dynamic configurations with your treatments, use the getTreatmentWithConfig methods. These methods returns an object containing the treatment and associated configuration.

The config element is a stringified version of the configuration JSON defined in Harness FME. If there is no configuration defined for a treatment, the SDK returns 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:

If you need to get multiple evaluations at once, you can also use getTreatmentsWithConfig methods. These methods take the exact same arguments as the getTreatments methods but return a mapping of feature flag names to splitResults instead of strings. Refer to the example below.

If a flag cannot be evaluated, the SDK returns the fallback treatment value (default "control" unless overridden globally or per flag). For more information, see Fallback treatments.

Append properties to impressions

Impressions are generated by the SDK each time a getTreatment method is called. These impressions are periodically sent back to Harness servers for feature monitoring and experimentation.

You can append properties to an impression by passing an object of key-value pairs to the getTreatment method. These properties are then included in the impression sent by the SDK and can provide useful context to the impression data.

Three types of properties are supported: strings, numbers, and booleans.

Shutdown

Before letting your app shut down, call destroy() as it gracefully shuts down the SDK by stopping all background threads, clearing caches, closing connections, and flushing the remaining unpublished impressions and events.

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:

After destroy() is called, any subsequent invocations to the client.getTreatment() or manager methods result in control or empty list respectively.

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 Harness FME and allows you to measure the impact of your feature flags on your users’ actions and metrics.

Learn more about using track events in feature flags.

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 Harness FME.

  • 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 used in creating the metric. This field can be sent in as null or 0 if you intend to only 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 in the Events guide. FME 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 sent back to Harness servers on the next event post. The SDK returns false if the current queue size is equal to the config set by eventsQueueSize or if an incorrect input to the track method is provided.

In the case that a bad input has been provided, you can read more about our SDK's expected behavior in our Events guide.

Manager

Use the Split Manager to get a list of feature flags available to the SDK factory client.

To instantiate a Manager in your code base, use the same factory that you used for your client.

The Manager then has the following properties and methods available.

The SplitView class referenced above has the following structure.

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 Harness servers for changes to feature flags at this rate (in seconds).

3600 seconds (1 hour)

segmentsRefreshRate

The SDK polls Harness servers for changes to segments at this rate (in seconds).

1800 seconds (30 minutes)

impressionRefreshRate

Controls how frequently the impressions cache expires after a write (in seconds). The treatment log captures which customer saw which treatment (on, off, etc.) and is periodically flushed back to Harness servers.

1800 seconds (30 minutes)

impressionsQueueSize

Default queue size for impressions.

30K

eventsPushRate

When using .track, how often the events queue is flushed to Harness 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

(optional) The default traffic type for events tracked using the track method. If not specified, every track call should specify a traffic type.

not set

telemetryRefreshRate

The SDK caches diagnostic data that it periodically sends to Harness servers. This configuration controls how frequently this data is sent back to Harness servers (in seconds).

3600 seconds (1 hour)

logLevel

Enables logging according to the level specified. Options are NONE, VERBOSE, DEBUG, INFO, WARNING, and ERROR.

NONE

synchronizeInBackground

Activates synchronization when application host is in background.

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 falls back to the polling mechanism. If false, the SDK polls for changes as usual without attempting to use streaming.

true

sync

Optional SyncConfig instance. Use it to filter specific feature flags to be synced and evaluated by the SDK. These filters can be created with the SplitFilter::bySet static function (recommended, flag sets are available in all tiers), or SplitFilter::byName static function, and appended to this config using the SyncConfig builder. If not set or empty, all feature flags are downloaded by the SDK.

null

offlineRefreshRate

The SDK periodically reloads the localhost mocked feature flags at this given rate in seconds. This can be turned off by setting it to -1 instead of a positive number.

-1 (off)

connectionTimeout

The timeout in seconds for establishing HTTP connections.

30 seconds

sdkReadyTimeOut

Amount of time in milliseconds to wait before notifying a timeout.

-1 (not set)

persistentAttributesEnabled

Enables saving attributes on persistent cache which is loaded as part of the SDK_READY_FROM_CACHE flow. All functions that mutate the stored attributes map affect the persistent cache.

false

syncEnabled

Controls the SDK continuous synchronization flags. When true, a running SDK processes rollout plan updates performed in Harness FME (default). When false, it fetches all data upon init, which ensures a consistent experience during a user session and optimizes resources when these updates are not consumed by the app.

true

impressionsMode

This configuration defines how impressions (decisioning events) are queued on the SDK. Supported modes are OPTIMIZED, NONE, and DEBUG. In OPTIMIZED mode, only unique impressions are queued and posted to Harness; this is the recommended mode for experimentation use cases. In NONE mode, no impression is tracked in Harness FME and only minimum viable data to support usage stats is, so never use this mode if you are experimenting with that instance impressions. Use NONE when you want to optimize for feature flagging only use cases and reduce impressions network and storage load. In DEBUG mode, ALL impressions are queued and sent to Harness; this is useful for validations. This mode doesn't impact the impression listener which receives all generated impressions locally.

OPTIMIZED

userConsent

User consent status used to control the tracking of events and impressions. Possible values are GRANTED, DECLINED, and UNKNOWN. See the User consent section for details.

GRANTED

encryptionEnabled

Enables or disables encryption for cached data.

false

httpsAuthenticator

If set, the SDK uses it to authenticate network requests. To set this value, an implementation of SplitHttpAuthenticator must be provided.

nil

prefix

Allows to use a prefix when naming the SDK storage. Use this when using multiple SplitFactory instances with the same SDK key.

nil

certificatePinningConfig

If set, enables certificate pinning for the given domains. For details, see the Certificate pinning section below.

null

rolloutCacheConfiguration

Specifies how long rollout data is kept in local storage before expiring.

null

To set each of the parameters defined above, use the syntax below:

Configure cache behavior

The SDK stores rollout data locally to speed up initialization and support offline behavior. By default, the cache expires after 10 days. You can override this or force clear the cache on SDK initialization.

The minimum value for cache expiration is 1 day. Any lower value will revert to the default of 10 days. Even if you enable the option to clear the cache on initialization, the SDK will only clear it once per day to avoid excessive network usage.

You can configure cache behavior using the rolloutCacheConfiguration setting:

  • expirationDays: Number of days to keep cached data before it is considered expired. Default: 10 days.

  • clearOnInit: If set to true, clears previously stored rollout data when the SDK initializes. Default: false.

Localhost mode

For testing, a developer can put code behind feature flags on their development machine without the SDK requiring network connectivity. To achieve this, the SDK can be started in 'localhost' mode. In this mode, the SDK neither polls nor updates Harness servers, rather it uses an in-memory data structure to determine what treatments to show to the customer for each of the features.

To use the SDK in localhost mode, replace the API Key with "localhost", as shown in the example below:

Since version 2.1.0, our SDK supports a new type of localhost feature flag definition file using the YAML format. This new format allows the user to map different keys to different treatments within a single feature flag, and add configurations to them. This file must be included into the project bundle and it is used as an initial file. It is copied to the cache folder, then it can be edited while app is running to simulate feature flag changes. When no file is added to the app bundle, an error occurs. The file periodically reloads. This period can be updated through the offlineRefreshRate config. Also, the refresh process can be turned off by setting this config to -1.

The new format is a list of single-key maps (one per mapping feature_flag-keys-config), defined as follows:

In the example above, we have four entries:

  • The first entry defines that for feature flag my_feature, the key key returns the treatment on and the on treatment is tied to the configuration {"desc" : "this applies only to ON treatment"}.

  • The second entry defines that the feature flag some_other_feature will always return the off treatment and no configuration.

  • The third entry defines that my_feature always returns off for all keys that don't match another entry. In this case, any key other than key.

  • The fourth entry shows how an example to override a treatment for a set of keys.

You can set the name of the localhost YAML file within cache folder as shown in the example below:

If SplitClientConfig.splitFile is not set, the 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:

Starting from version 2.24.2, it is possible to update feature flag definitions programmatically by using the Localhost factory's updateLocalhost method, as shown below.

FEATURE_FLAG_NAME TREATMENT

Additionally, you can include comments to the file starting a line with the ## character.

Example: A sample localhost.splits file

By enabling debug mode, the localhost file location is logged to the console so that it's 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.

Listener

FME SDKs send impression data back to Harness servers periodically and as a result of evaluating feature flags. 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. Refer to the snippet below.

In regards with the data available here, refer to the impression objects interface and description of each field below. There are two fields in particular that are different for Swift and Obj-C so see the corresponding tab:

Name

Type

Description

keyName

String?

Key used for targeting (matching). Also used for bucketing unless a specific bucketingKey was provided.

bucketingKey

String?

Optional. Key used to control rollout assignment (bucketing) separately from the matching key.

feature

String?

Feature flag which is evaluated.

treatment

String?

Treatment that is returned.

time/timestamp

Int64?/NSNumber?

Timestamp of when the impression is generated.

label

String?

Targeting rule in the definition that matched resulting in the treatment being returned.

changeNumber/changeNum

Int64?/NSNumber?

Date and time of the last change to the targeting rule that the SDK used when it served the treatment. It is important to understand when a change made to a feature flag got picked up by the SDKs and whether one of the SDK instances is not picking up changes.

attributes

[String: Any]?

A map of attributes passed to getTreatment/getTreatments, if any.

Flush

The flush() method sends the data stored in memory (impressions and events) to Harness FME servers and clears the successfully posted data. If a connection issue is experienced, the data will be sent on the next attempt.

Background synchronization

Since version 2.11.0, background synchronization is available for devices having iOS 13+. To enable this feature, just follow the next 4 steps:

  1. Enable Background Mode Fetch capability for your app.

  2. Add the SDK background sync task identifier io.split.bg-sync.task to the Permitted background task scheduler identifiers section of the Info.plist .

  3. Set the Split config flag synchronizeInBackground to true .

  1. Schedule the background sync during app startup. e.g., application(_:didFinishLaunchingWithOptions:)

Logging

To enable SDK logging, the logLevel setting is available in SplitClientConfig class:

The following shows an example output:

Configure fallback treatments

Fallback treatments let you define a treatment value (and optional configuration) to be returned when a flag cannot be evaluated. By default, the SDK returns control, but you can override this globally or for individual flags at the SDK level.

This is useful when you want to:

  • Maintain a predictable user experience during outages or evaluation failures (avoid unexpected control in production)

  • Protect critical user flows by returning a safe, stable treatment (for example, forcing off during an incident)

  • Customize behavior per flag so each evaluation inherits appropriate safe defaults if something goes wrong

Global fallback treatment

Set a global fallback treatment when initializing the SDK factory. This value is returned whenever any flag cannot be evaluated.

Flag-level fallback treatment

You can also set a fallback treatment per flag when calling getTreatment or getTreatmentWithConfig. This flag-level fallback always takes precedence over the global fallback treatment, so if both are defined, the SDK will return the flag-level value when that flag cannot be evaluated.

For more information, see Fallback treatments.

Advanced use cases

This section describes advanced use cases and features provided by the SDK.

Instantiate multiple SDK clients

In versions previous to 2.14.0, you had to create more that one SDK instance to evaluate for different users IDs. From v2.14.0 on, FME supports the ability to create multiple clients, one for each user ID. For example, if you need to roll out feature flags for different user IDs, you can instantiate multiple clients, one for each ID. You can then evaluate them using the corresponding client. You can do this using the example below:

NUMBER OF SDK INSTANCES

While the SDK does not put any limitations on the number of instances that you can create, we strongly recommend keeping the number of SDKs down to one or two.

Subscribe to events

You can listen for four different events from the SDK.

  • sdkReadyFromCache. This event fires once the SDK is ready to evaluate treatments. The SDK will use a locally cached version of your rollout plan from a previous session if available. In this case, the event fires almost immediately, since access to the cache is fast, but data might be stale. Otherwise, it fires together with the sdkReady event when the SDK downloads the rollout plan from Harness servers.

  • sdkReady. This event fires once the SDK is ready to evaluate treatments using the most up-to-date version of your rollout plan, downloaded from Harness servers.

  • sdkReadyTimedOut. This event fires if the SDK could not fully download the data from Harness servers (sdkReady event) within the time specified by the sdkReadyTimeOut property of the SplitClientConfig object. This event does not indicate that the SDK initialization was interrupted. The SDK continues downloading the rollout plan and fires the sdkReady event when finished. This delayed sdkReady event may happen with slow connections or large rollout plans with many feature flags, segments, or dynamic configurations.

  • sdkUpdated. This event fires whenever your rollout plan is changed. Listen for this event to refresh your app whenever a feature flag or segment is changed in Harness FME.

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.

Include metadata

SUPPORTED SDK VERSIONS

Event metadata is supported in the iOS SDK version 3.7.0 or later.

metadata provides additional context for events:

  • onSdkReady / onSdkReadyFromCache: Includes isInitialCacheLoad (true if no cached data from a previous session was available) and lastUpdateTimestamp (milliseconds since epoch when the cache was last updated).

  • onSdkUpdate: Includes type (.flagsUpdate or .segmentsUpdate) and names (list of impacted flags; empty for segment-only updates).

For example:

The SDK allows you to disable the tracking of events and impressions until user consent is explicitly granted or declined.

The userConsent configuration parameter lets you set the initial consent status of the SDK instance, and the factory method setUserConsent(enabled: Bool) lets you grant (enable) or decline (disable) dynamic data tracking.

There are three possible initial states:

  • 'GRANTED': The user grants consent for tracking events and impressions. The SDK sends them to Harness FME servers. This is the default value if userConsent param is not defined.

  • 'DECLINED': The user declines consent for tracking events and impressions. The SDK does not send them to Harness FME servers.

  • 'UNKNOWN': The user neither grants nor declines consent for tracking events and impressions. The SDK tracks them in its internal storage, and eventually either sends them or not if the consent status is updated to 'GRANTED' or 'DECLINED' respectively.

The status can be updated at any time with the setUserConsent factory method.

Working with user consent is demonstrated below.

Certificate pinning

The SDK allows you to constrain the certificates that the SDK trusts, using one of the following techniques:

  1. Pin a certificate's SubjectPublicKeyInfo, by providing the public key as a base64 SHA-256 hash or a base64 SHA-1 hash.

  2. Pin a certificate's entire certificate chain (the root, all intermediate, and the leaf certificate), by providing the certificate chain as a .der file.

Each pin corresponds to a host. For subdomains, you can optionally use wildcards, where * will match one subdomain (e.g. *.example.com), and ** will match any number of subdomains (e.g **.example.com).

You can optionally configure a handler to execute on certificate validation failure for a host.

To set the SDK to require pinned certificates for specific hosts, add the CertificatePinningConfig object to SplitClientConfig, as shown below.

In iOS SDK version 3.5.0 or later, you can observe the full outcome of the certificate pinning process, in addition to failures, by adding a status handler that reports:

  • Whether certificate pinning succeeded

  • Whether pinning failed (and why)

  • Whether the SDK fell back to default OS handling

The status handler returns two values:

  • status: The high-level outcome of certificate pinning (success, failed, or defaultHandling)

    Status
    Meaning

    success

    Certificate pinning succeeded

    failed

    Pinning failed and the connection was rejected

    defaultHandling

    Pinning was not applied and default OS handling was used

  • reason: A string describing the underlying result

    Status Category
    Status Value
    Description

    Success

    success

    Certificate successfully matched a pinned credential

    Failed

    error

    Error validating credentials

    Failed

    invalidChain

    Certificate chain is invalid

    Failed

    credentialNotPinned

    Certificate does not match any pinned credential

    Failed

    spkiError

    Unable to extract SPKI from the public key

    Failed

    invalidCredential

    Invalid certificate credentials

    Failed

    invalidParameter

    Incorrect credential type or parameter

    Failed

    unavailableServerTrust

    Server trust information unavailable

    Default handling

    noPinsForDomain

    No pins configured for the requested host

    Default handling

    noServerTrustMethod

    Validation method is not Server Trust

Applications should rely on the status value for control flow and use the reason value for logging and diagnostics. This allows you to log, audit, and monitor all pinning outcomes, including cases where no pins are configured for a domain.

Troubleshooting

Runtime error in JFBCrypt.m: left shift cannot be represented in type 'SInt32'

When using the iOS SDK in an Objective-C project, you might encounter a runtime error immediately after initializing the SDK factory, similar to: runtime error: left shift of 16488694 by 8 places cannot be represented in type 'SInt32' (aka 'int') reported in JFBCrypt.m.

This error occurs if the Undefined Behavior Sanitizer (UBSan) flag is enabled for your build. UBSan detects undefined behaviors in code, and this particular bit shift triggers the sanitizer.

To fix this issue:

  1. Disable the Undefined Behavior Sanitizer flag by navigating to your target’s Edit Scheme > Diagnostics tab and unchecking the Undefined Behavior Sanitizer option.

  2. Clean your project build.

  3. Delete the Derived Data folder to remove cached build artifacts.

  4. Rebuild your project.

This will prevent the sanitizer from flagging the bit shift as an error and allow the SDK to initialize correctly.

Is the iOS SDK Split library missing the track method?

When using the iOS SDK in an Xcode project, attempting to call the track method results in a build error:

This error usually occurs because the iOS SDK version used is older than 1.3.0, which did not include the track method.

Update the iOS SDK to the latest version via CocoaPods.

Last updated

Was this helpful?