Skip to content
The mobile SDKs are available on request. Email support@grona.ai to get access.

Quickstart: iOS

This quickstart adds the Grona iOS SDK to your app, starts it when the app launches, and shows Grona content in the home_top placement on your home screen. At the end you check the result in the SDK’s debug view. It takes one Swift package, one host type, one call at launch and one view on the screen.

The iOS SDK is at version 0.1.0. It supports iOS 16 and later, and needs Swift 5.10 or later.

Grona sets up your workspace for apps and gives you these values. The examples on this page use Acme Market’s.

Value Example
Workspace key acme-market
Config URL https://api.grona.ai/v1/mobile/workspaces/acme-market/config
Events URL https://api.grona.ai/v1/mobile/events
Preview pairing URL https://api.grona.ai/v1/mobile/preview/pair
Configuration key id and public key acme-market-2026-10 and a base64 public key
Signed bundled configuration A grona-config.json file
  1. Add the package. In Xcode, choose File > Add Package Dependencies, enter https://github.com/GronaAI/grona-ios, and set Dependency Rule to Exact Version 0.1.0. Add the Grona and GronaDebug products to your app target. For a Package.swift setup and the requirements, see Install the SDK.

  2. Add the bundled configuration. Drag the signed grona-config.json from Grona into your project and tick your app target. The SDK applies it at launch and when the device is offline, before the first download from the config URL.

  1. Implement GronaHost. The SDK calls it on the main actor to look up products, add them to the basket, replace a basket line and open screens. This minimal version knows no products and accepts no actions, which is enough for a first banner or message without a call to action:

    AcmeGronaHost.swift
    import Grona
    // A minimal host. The full version in Handle actions takes your app's services.
    @MainActor
    final class AcmeGronaHost: GronaHost {
    func product(id: String) -> GronaProduct? { nil }
    func add(_ lines: [GronaLine], source: GronaSource) -> GronaAddResult { .rejected }
    func replace(productID: String, with line: GronaLine, source: GronaSource) -> GronaAddResult { .rejected }
    func navigate(to route: GronaRoute, source: GronaSource) -> Bool { false }
    }

    Content that names products or adds them to the basket shows nothing until product(id:) returns your products from a fast in-memory lookup and add adds them. See Handle actions.

  2. Call Grona.start at launch. In a SwiftUI app, call it from your App initialiser with the values from Grona. For an app delegate, see Initialise the SDK.

    AcmeMarketApp.swift
    import Grona
    import SwiftUI
    @main
    struct AcmeMarketApp: App {
    // Base64 DER SubjectPublicKeyInfo from Grona; a public key, not a secret
    private static let configPublicKey = "REPLACE_WITH_CONFIG_PUBLIC_KEY_FROM_GRONA"
    init() {
    do {
    let options = try GronaOptions(
    workspaceKey: "acme-market",
    configURL: URL(string: "https://api.grona.ai/v1/mobile/workspaces/acme-market/config"),
    bundledConfig: Bundle.main.url(forResource: "grona-config", withExtension: "json"),
    configKeys: ["acme-market-2026-10": Self.configPublicKey],
    eventsURL: URL(string: "https://api.grona.ai/v1/mobile/events"),
    previewURL: URL(string: "https://api.grona.ai/v1/mobile/preview/pair")
    )
    Grona.start(options: options, host: AcmeGronaHost())
    } catch {
    assertionFailure("Invalid Grona options: \(error)")
    }
    }
    var body: some Scene {
    WindowGroup {
    HomeView()
    }
    }
    }

    The GronaOptions initialiser throws GronaOptionsError if there is no configuration key, if a URL is not https, or if the workspace key is not valid. After that, the SDK never throws into your app. Grona.start returns at once and does its work off the main thread. It is ignored on repeat calls and in app extensions.

  3. Pass the shopper’s consent answer. Consent starts as .unknown on every launch: events stay in memory and no events are sent. Call setConsent after start with the answer from your own consent flow, and again whenever it changes:

    import Grona
    func analyticsConsentChanged(_ allowed: Bool?) {
    guard let allowed else {
    Grona.setConsent(.unknown)
    return
    }
    Grona.setConsent(allowed ? .granted : .denied)
    }

    Consent controls events, not rendering: Grona content still shows under .denied, and nothing is collected. See Consent.

  1. Add a slot where the content should appear, with the placement home_top and the home screen:

    HomeView.swift
    import Grona
    import SwiftUI
    struct HomeView: View {
    var body: some View {
    ScrollView {
    VStack(spacing: 0) {
    GronaSlot(placement: "home_top", screen: .home)
    // The rest of your home screen
    }
    }
    }
    }
  2. Run the app. If the configuration has a campaign for home_top and you are in its treatment arm, the component appears at the top of the home screen.

Open the debug view from a debug-only menu or button in your app, inside #if DEBUG:

#if DEBUG
import GronaDebug
#endif
// In a debug-only menu of a view controller
#if DEBUG
Grona.presentDebugView(from: self)
#endif

In SwiftUI, present GronaDebugView() in a sheet instead. The view shows the configuration the SDK applied, the slots on screen, each campaign with your arm, and recent events. It opens only when GronaDebug was compiled with the DEBUG condition; otherwise the call does nothing. See Test and debug.

If the slot is empty, check your arm for the campaign. Control and holdout show nothing by design. To see the treatment on your device, pin the variant locally from the debug view or open a preview link.