iOS SDK App Features

Preparation

Before working with App Features, configure and initialize MagifyClient.

Create the client, subscribe to onUpdate, and then call setup():

let magify = MagifyClient(
    for: "your_app_name",
    defaultConfigURL: Bundle.main.url(
        forResource: "default_config",
        withExtension: "json"
    )!,
    isSandbox: false
)

magify.features.onUpdate = {
    // Re-read your features here.
}

magify.setup()

onUpdate is called whenever the resolved feature values change, for example when a new remote configuration is received or the user context changes.

Overview

App Features allow you to customize application behavior remotely without releasing a new version of your application.

Depending on the user state, segmentation rules, or A/B test participation, different feature values can be delivered automatically.

The SDK provides a centralized way to manage application settings, parameters, and behavior through remote features.

The SDK is responsible for:

  1. Downloading and Caching — downloading feature data from the server, storing it locally, and reusing cached data without repeated requests.
  2. Reactivity — tracking feature changes through the onUpdate callback, making it easy to synchronize application state between business logic and the user interface.
  3. Flexibility — working with different data types, including booleans, numbers, strings, dictionaries, and custom Decodable objects.
  4. Relevance — automatically updating feature values whenever the user's state changes in a way that affects the resolved configuration.

Basic Usage

The SDK exposes two feature providers:

magify.features        // Value Features, available immediately
magify.storedFeatures  // Stored App Features, loaded and cached asynchronously

The main difference between them is how feature values are delivered.

  • Value Features are included directly in the remote configuration and become available as soon as the configuration is loaded.
  • Stored App Features are delivered as separate files. The remote configuration contains a URL for each stored feature, and the SDK downloads and caches the corresponding file automatically.

Usually, Value Features are sufficient for most configuration scenarios. When you need to deliver larger customization data, package it as a separate file and use Stored App Features instead.

Both providers are accessed in exactly the same way — they conform to the same Features protocol:

public protocol Features: AnyObject {
    var onUpdate: (() -> Void)? { get set }
    var allKeys: [String] { get }
    var allValues: [String: Any?] { get }

    func value(forKey key: String) -> Any?
    func string(forKey key: String) -> String?
    func bool(forKey key: String) -> Bool?
    func int(forKey key: String) -> Int?
    func double(forKey key: String) -> Double?
    func dictionary(forKey key: String) -> [String: Any?]?
    func customObject<T>(
        _ type: T.Type,
        forKey key: String
    ) throws -> T? where T: Decodable
    func getCustomJsonData(forKey key: String) -> String?
}

Every typed getter returns an optional value. It is nil when the key is missing or when the stored value is not of the requested type, so always provide your own default value when appropriate using ??.

Value Features

Value Features are provided through magify.features.

Their values are delivered as part of the configuration and can be read through typed getters.

The examples below use:

let features = magify.features

and the following feature configuration:

{
  "app_features": {
    "title": {
      "default": "Summer sale",
      "options": []
    },
    "is_enabled": {
      "default": true,
      "options": []
    },
    "max_lives": {
      "default": 5,
      "options": []
    },
    "reward": {
      "default": 2.5,
      "options": []
    },
    "theme": {
      "default": {
        "primary": "#FF0000",
        "dark": true
      },
      "options": []
    },
    "tabs": {
      "default": ["home", "shop"],
      "options": []
    }
  }
}

Reading a Single Value

Every getter returns an optional value. It is nil when the key is missing or the value is not of the requested type.

value(forKey:)

Returns the resolved value as a plain Swift value: String, Int, Bool, Double, [String: Any?], or [Any?].

Returns nil when the key is in neither configuration or cannot be resolved yet.

features.value(forKey: "title") // "Summer sale" as Any

string(forKey:)

Returns the value when it is a string, or nil otherwise.

features.string(forKey: "title") // "Summer sale"

bool(forKey:)

Returns the value when it is a boolean, or nil otherwise.

features.bool(forKey: "is_enabled") // true

int(forKey:)

Returns the value when the configuration contains a whole number, or nil otherwise.

features.int(forKey: "max_lives") // 5

double(forKey:)

Returns the value when the configuration contains a number with a fractional part, or nil otherwise.

features.double(forKey: "reward") // 2.5

int and double do not overlap: 5 can be read only through int, while 2.5 can be read only through double.

If either type may be used:

let n = features.double(forKey: "reward")
    ?? features.int(forKey: "reward").map(Double.init)
    ?? 0

dictionary(forKey:)

Returns the value when it is a JSON object.

Returns nil for arrays and scalar values. Nested JSON null values remain present as nil entries.

features.dictionary(forKey: "theme")
// ["primary": "#FF0000", "dark": true]

customObject(_:forKey:)

Decodes the feature directly into your own Decodable type.

Use this method instead of dictionary(forKey:) when working with structured data.

It returns nil when the key has no value and throws the decoder error when the data structure does not match the target type. This is most commonly DecodingError.keyNotFound after a configuration change.

struct Theme: Decodable {
    let primary: String
    let dark: Bool
}

let theme = try? features.customObject(
    Theme.self,
    forKey: "theme"
)
// Theme(primary: "#FF0000", dark: true)

getCustomJsonData(forKey:)

Returns a single feature value as raw JSON text.

Use it when you need to pass the value to a decoder, web view, or log.

Containers are returned as JSON objects or arrays. Scalar values are returned as JSON fragments, so strings are returned with quotes.

Only the requested key is read.

features.getCustomJsonData(forKey: "theme")
// "{\"primary\":\"#FF0000\",\"dark\":true}"

Reading the Whole Set

allKeys

Returns the keys of all currently resolved features.

let keys = features.allKeys

allValues

Returns a snapshot of all currently resolved feature values.

let values = features.allValues

allValuesJson()

Returns the whole resolved feature set as a single JSON string.

let json = features.allValuesJson()

Reacting to Changes

onUpdate

Use onUpdate when you need to react to changes across the resolved feature set.

features.onUpdate = {
    // Re-read the feature values you need.
}

When onUpdate is called, re-read the current values from the feature provider.

Per-feature subscriptions

Use subscribe(forKey:) when you need to observe changes to a specific feature.

let subscription = features.subscribe(forKey: "title") { value in
    // Handle the updated value.
}

The SDK also provides typed subscriptions:

let boolSubscription = features.subscribeBool(forKey: "is_enabled") { value in
    // value is Bool?
}

let intSubscription = features.subscribeInt(forKey: "max_lives") { value in
    // value is Int?
}

let doubleSubscription = features.subscribeDouble(forKey: "reward") { value in
    // value is Double?
}

let stringSubscription = features.subscribeString(forKey: "title") { value in
    // value is String?
}

Each subscription returns a FeatureSubscription.

Keep the returned subscription for as long as you need to receive updates.

Cancel the subscription when updates are no longer needed:

subscription.cancel()

Ignoring Features

Use setIgnoredFeatures(_:source:) to ignore selected feature keys.

The source parameter determines which configuration source the ignore list applies to:

  • .current — ignore the selected features in the current configuration.
  • .default — ignore the selected features in the default configuration.
  • .both — ignore the selected features in both configurations.
magify.setIgnoredFeatures(
    ["title", "reward"],
    source: .current
)

To clear the ignored feature list:

magify.clearIgnoredFeatures()

Reading a Single Source

Use source-specific snapshots when you need to inspect the current and default feature values separately.

allCurrentValues

Returns the raw feature values from the current configuration.

let currentValues = features.allCurrentValues

allDefaultValues

Returns the raw feature values from the bundled default configuration.

let defaultValues = features.allDefaultValues

Stored App Features

Each Stored App Feature value is a separate downloaded file rather than a value included directly in the configuration.

Use Stored App Features for large payloads, such as a large JSON object or a level layout, that you would not want to include in the main configuration.

Access Stored App Features through:

magify.storedFeatures

The SDK downloads and caches these files. Content cached from a previous application run is available at launch, while fresh content is fetched in the background when it changes.

A Stored App Feature resolves to the content of its downloaded file.

Every getter returns an optional value — nil when the key is unknown or when its file has not finished downloading yet.

In the configuration, a Stored App Feature's default value, as well as each option value, is a URL, not the feature value itself.

The SDK downloads the file behind that URL and provides its content as the feature value:

{
  "stored_app_features": {
    "promo_banner": {
      "default": "https://cdn.magify.com/features/promo_banner.txt",
      "options": []
    },
    "promo_enabled": {
      "default": "https://cdn.magify.com/features/promo_enabled.json",
      "options": []
    },
    "max_level": {
      "default": "https://cdn.magify.com/features/max_level.json",
      "options": []
    },
    "spawn_rate": {
      "default": "https://cdn.magify.com/features/spawn_rate.json",
      "options": []
    },
    "startup": {
      "default": "https://cdn.magify.com/features/startup.json",
      "options": []
    }
  }
}

The files behind these URLs contain the actual values:

promo_banner.txt   → Summer sale!
promo_enabled.json → true
max_level.json     → 5
spawn_rate.json    → 2.5
startup.json       → { "lives": 5, "coins": 100 }

The examples below use:

let storedFeatures = magify.storedFeatures

Reading a Single Value

value(forKey:)

Returns the file content as a plain Swift value: String, Int, Bool, Double, [String: Any?], or [Any?].

Returns nil when the key is unknown or its file is not ready.

storedFeatures.value(forKey: "promo_banner")
// "Summer sale!" as Any

string(forKey:)

Returns the content when the file contains a string, or nil otherwise.

storedFeatures.string(forKey: "promo_banner")
// "Summer sale!"

bool(forKey:)

Returns the content when the file contains a boolean, or nil otherwise.

storedFeatures.bool(forKey: "promo_enabled")
// true

int(forKey:)

Returns the content when the file contains a whole number, or nil otherwise.

storedFeatures.int(forKey: "max_level")
// 5

double(forKey:)

Returns the content when the file contains a fractional number, or nil otherwise.

int and double do not overlap — a whole number can be read only through int, while a fractional number can be read only through double.

storedFeatures.double(forKey: "spawn_rate")
// 2.5

dictionary(forKey:)

Returns the content when the file contains a JSON object, or nil otherwise.

storedFeatures.dictionary(forKey: "startup")
// ["lives": 5, …]

customObject(_:forKey:)

Decodes the file directly into your own Decodable type.

This is the preferred way to read a JSON object file.

Returns nil when the file is not ready and throws the decoder error when the data structure does not match the target type.

let config = try? storedFeatures.customObject(
    StartupConfig.self,
    forKey: "startup"
)
// StartupConfig(...)

getCustomJsonData(forKey:)

Returns the file content as raw JSON text, for example, when you need to pass it to a web view.

Only the requested key's file is read.

storedFeatures.getCustomJsonData(forKey: "startup")
// "{\"lives\":5,...}"

Reading the Whole Set

allKeys

Returns the sorted list of every Stored App Feature key.

Accessing allKeys does not read any files from disk.

storedFeatures.allKeys
// ["max_level", "promo_banner", "promo_enabled", "spawn_rate", "startup"]

allValues

Returns a [String: Any?] snapshot of every Stored App Feature, with each value resolved in the same way as value(forKey:).

Keys whose files are not ready map to nil.

storedFeatures.allValues
// ["promo_banner": "Summer sale!", "max_level": 5, "startup": [...], …]

allValuesJson()

Returns every ready Stored App Feature as a single JSON object string.

Keys whose files are not ready are omitted. An empty set returns "{}".

storedFeatures.allValuesJson()
// {"promo_banner":"Summer sale!","max_level":5,"startup":{...},…}

allValues and allValuesJson() read every stored file at once. Avoid using them on hot paths; read only the specific key you need instead.

Reacting to Changes

onUpdate

onUpdate is called whenever a stored file finishes downloading or its content changes.

Re-read the required feature values inside the callback.

onUpdate is a single closure — assigning a new closure replaces the previous one.

storedFeatures.onUpdate = { [weak self] in
    DispatchQueue.main.async {
        self?.applyCriticalFeatures()
    }
}

Reading at Launch

A file may become available only a moment after application launch.

Read the value after onUpdate fires rather than assuming that it is ready at the beginning of your bootstrap process.

Content cached from a previous run is available immediately. Re-read the values after the SDK finishes synchronizing Stored App Features:

func start() {
    // Whatever is cached from a previous run is available right away.
    applyCriticalFeatures()

    // Re-read once the SDK finishes syncing.
    magify.storedFeatures.onUpdate = { [weak self] in
        DispatchQueue.main.async {
            self?.applyCriticalFeatures()
        }
    }
}

func applyCriticalFeatures() {
    let config = try? magify.storedFeatures.customObject(
        StartupConfig.self,
        forKey: "startup"
    )

    // Load your game with these values.
}

Preloading

There is no preloading flag.

The SDK automatically downloads and caches Stored App Feature files in the background.

Preloading occurs:

  • on cold start;
  • after a new remote configuration is received;
  • when relevant user state changes.

When a Stored App Feature is resolved, the SDK downloads the file associated with its URL and stores it in the local cache.

Cached content from a previous application run can be used immediately while updated content is fetched in the background.

When a stored file finishes downloading or its content changes, storedFeatures.onUpdate is called.

A Stored App Feature value may therefore not be available immediately when the application starts. Read the cached value when available and re-read the feature after onUpdate fires.

Related articles

iOS SDK Privacy & Consent

IServicePrefs

RewardProduct

PurchaserService

Android SDK Releases

InfoProduct