Return Types & Models

Variation/Feature models, FeatureStatus, GoalData keys, and the never-throws / nil-on-miss contract

This page documents the public Swift types the SDK returns and the enums you pass in. All model types live in ConvertSwiftSDKCore; the event constants live in ConvertSwiftSDKCore/Event. The SDK never throws from its public decision methods — absent or not-ready data surfaces as nil, an empty array, or a disabled Feature.

Variation

Returned by ConvertContext.runExperience(...) (optional) and runExperiences(...) (as an array). A Swift struct conforming to Codable, Sendable, and Identifiable:

public struct Variation: Codable, Sendable, Identifiable {
    public let id: String
    public let key: String
    public let experienceId: String
    public let experienceKey: String
}
  • key — the merchant-defined variation key from the Convert dashboard; this is what you branch on.
  • id — the variation identifier (typed String, not Int, for wire forward-compatibility).
  • experienceId / experienceKey — the owning experience's identifier and key.

Unlike the Android model, all fields are non-optional — the iOS Variation is only returned when a complete record can be constructed; a missing or not-bucketed visitor returns nil from runExperience.

if let variation = await context.runExperience("homepage-redesign") {
    switch variation.key {
    case "control":   renderControl()
    case "treatment": renderTreatment()
    default:          renderControl()
    }
} else {
    renderControl() // not ready / not bucketed / experience absent
}

Feature and FeatureStatus

Returned by ConvertContext.runFeature(...) (non-optional) and runFeatures(...) (as an array). A Swift struct conforming to Codable, Sendable, and Equatable:

public struct Feature: Codable, Sendable, Equatable {
    public let id: String
    public let key: String
    public let status: FeatureStatus
    public let variables: [String: FeatureVariable]
}

public enum FeatureStatus: String, Codable, Sendable, Equatable {
    case enabled
    case disabled
}
  • status == .enabled means the feature is on for this visitor and variables holds the bucketed values.
  • When status == .disabled, variables is an empty dictionary and variable accessors return nil.
  • runFeature is non-optional by contract: a missing snapshot or miss returns Feature.disabled(key:) rather than nil — never throws.
let feature = await context.runFeature("checkout-v2")
if feature.status == .enabled {
    enableCheckoutV2()
}

Feature.disabled(key:)

The canonical "feature off" factory:

public static func disabled(key: String) -> Feature

Returns a Feature with status == .disabled and an empty variables dictionary. Used internally by the degraded path; you can use it in tests or as a safe default.

Feature variable accessors

When a feature is .enabled, variables holds FeatureVariable values — a Swift enum with five cases mirroring the five JS variable types:

public enum FeatureVariable: Codable, Sendable, Equatable {
    case boolean(Bool)
    case integer(Int)
    case float(Double)
    case string(String)
    case json(Data)   // raw JSON bytes; decode at the call site
}

Use the non-throwing typed accessor variable(_:as:) to read a variable by name:

public func variable<T>(_ key: String, as type: T.Type) -> T?
let feature = await context.runFeature("checkout-v2")
let color   = feature.variable("ctaColor", as: String.self) ?? "#0066ff"
let limit   = feature.variable("maxItems", as: Int.self) ?? 20
let price   = feature.variable("price", as: Double.self) ?? 9.99
let flag    = feature.variable("experimental", as: Bool.self) ?? false

For .json variables, pass Data.self; then decode the bytes at the call site using JSONDecoder or another decoder:

if let raw = feature.variable("config", as: Data.self) {
    let decoded = try JSONDecoder().decode(MyConfig.self, from: raw)
}

variable(_:as:) returns nil when the key is unknown or when the stored case's associated value is not of the requested type T. It never force-unwraps.

GoalData, GoalDataKey, and GoalDataValue

GoalData is a type alias for a dictionary:

public typealias GoalData = [GoalDataKey: GoalDataValue]

Passed to ConvertContext.trackConversion(...) to attach transactional data to a conversion event.

GoalDataKey is an eight-case String-backed enum:

CaseWire name (rawValue)Typical value
.amountamountnumber
.productsCountproductsCountinteger
.transactionIdtransactionIdstring
.customDimension1customDimension1string / number
.customDimension2customDimension2string / number
.customDimension3customDimension3string / number
.customDimension4customDimension4string / number
.customDimension5customDimension5string / number

GoalDataValue is a three-case enum encoding the possible metric value types:

public enum GoalDataValue: Codable, Sendable {
    case double(Double)
    case string(String)
    case strings([String])
}

Build a GoalData dictionary literal — no constructor needed:

let data: GoalData = [
    .amount:        .double(49.99),
    .productsCount: .double(3),
    .transactionId: .string("tx-42"),
]
await context.trackConversion("purchase-completed", goalData: data)

ConvertError

The single error type thrown by the SDK (LocalizedError, Sendable):

public enum ConvertError: LocalizedError, Sendable {
    case invalidConfiguration(String)
    case invalidSdkKey(String)
}

Only ready() and its completion overload throw ConvertError. All decisioning methods (runExperience, runFeature, trackConversion, etc.) never throw — degraded inputs surface as nil, an empty array, or a disabled Feature.

CaseWhen thrown
.invalidConfiguration(String)Invalid config struct fields, or empty/invalid direct-data payload
.invalidSdkKey(String)Structurally invalid or empty SDK key
do {
    try await sdk.ready()
} catch let error as ConvertError {
    print(error.errorDescription ?? "unknown")
}

LogLevel

public enum LogLevel: String, CaseIterable, Comparable, Sendable {
    case trace
    case debug
    case info
    case warn    // default production level
    case error
    case silent  // fully mutes output
}

Comparable compares by severity: trace < debug < info < warn < error < silent. A logger configured at a given level emits messages at that level and above. The default is .warn. Set via ConvertConfiguration(sdkKey:logLevel:). See Configuration.

SystemEvent

Well-known event names you can subscribe to with sdk.on(_:callback:). Each is a case on the SystemEvent enum with a JS-parity raw String value:

CaseRaw value (wire string)
.readyready
.configUpdatedconfig.updated
.apiQueueReleasedapi.queue.released
.bucketingbucketing
.conversionconversion
.segmentssegments
.locationActivatedlocation.activated
.locationDeactivatedlocation.deactivated
.audiencesaudiences
.dataStoreQueueReleaseddatastore.queue.released

The set is frozen — the 10 members and their raw values match the JS SDK exactly and will not change.

EventListenerToken

public struct EventListenerToken: Sendable, Hashable, Equatable

An opaque subscription handle returned by sdk.on(_:callback:). Pass it to sdk.off(_:) to cancel. Tokens are created only by the SDK; callers cannot mint their own.

let token = await sdk.on(.conversion) { payload in
    // handle conversion payload
}
await sdk.off(token)

Segments

public struct Segments: Codable, Sendable, Equatable {
    public var country: String?
    public var browser: String?
    public var devices: String?
    public var source: String?
    public var campaign: String?
    public var visitorType: String?
    public var customSegments: [String]?
}

Set via ConvertContext.setDefaultSegments(_:) (string-keyed fields) and setCustomSegments(_:) (the customSegments array). Both methods accept async; completion-handler overloads are not provided for segmentation methods. See Code Examples.

ConvertValue

public enum ConvertValue: Sendable, Equatable {
    case string(String)
    case int(Int)
    case double(Double)
    case bool(Bool)
}

The closed set of scalar attribute types the SDK accepts in createContext(attributes:). Unsupported values (nested dictionaries, arrays, NSNull, custom objects) are dropped at createContext call time and logged at debug. Accessible indirectly through ConvertContext.attributes as [String: Any].

Next steps