Initialise the SDK
This page starts the SDK when your app launches. You build a GronaOptions value, implement GronaHost so Grona can reach your cart and router, and call Grona.start once, from your SwiftUI App initialiser or your app delegate. Every other Grona call does nothing until start has run.
Start Grona at launch
Section titled “Start Grona at launch”Grona provides the workspace key, the URLs and the signing keys for your workspace. The values below are Acme Market’s; replace them with yours.
-
Add the signed bundled configuration that Grona exports for your workspace to your app target, named
grona-config.json. Drag it into Xcode’s project navigator, tick your app target, and check that it is listed under Build Phases > Copy Bundle Resources. The SDK applies it at launch and whenever it is offline. -
Build the options and call
Grona.start:AcmeMarketApp.swift import Gronaimport SwiftUI@mainstruct AcmeMarketApp: App {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": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."],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(services: .shared))} catch {assertionFailure("Invalid Grona options: \(error)")}}var body: some Scene {WindowGroup {HomeView()}}}AppDelegate.swift import Gronaimport UIKit@mainfinal class AppDelegate: UIResponder, UIApplicationDelegate {func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {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": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."],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(services: .shared))} catch {assertionFailure("Invalid Grona options: \(error)")}return true}} -
Implement
AcmeGronaHost(outline below), then pass the shopper’s consent, identity and basket as your app learns them.
How start behaves
Section titled “How start behaves”- Constant time.
startqueues its work off the main thread and returns. It does no disk or network work on the main thread. - Never throws. If start-up fails, the SDK logs through your
GronaLoggerand stays inactive. Your app carries on. - Once only. A second call is ignored, so you cannot change options after launch.
- Your app only. In an app extension
startdoes nothing and Grona stays inactive there. - Before
start. Every other call does nothing.Grona.decisionreturns a subscription that never delivers.Grona.refresh(completion:)still calls its completion, andawait Grona.refresh()still returns. - Concurrency.
startandGrona.decisionare main-actor APIs, and Swift checks at compile time that you call them on the main actor. Setters such assetBasketandidentifyare safe from any thread. The SDK calls yourGronaHoston the main actor only. - In order. The SDK applies your calls in the order you make them from any one thread, including calls made right after
start. SosetConsentstraight afterstart, oridentifyfollowed bysetProfile, behaves as written. - Your host stays alive. The SDK keeps a strong reference to the host for the life of the process. Make it a long-lived object, not a view controller.
- Before the first passcode. If iOS launches your app in the background after a restart, before the shopper has entered their passcode, the SDK’s files cannot be read yet. The SDK waits until they can be, rather than starting with a new install id.
At start the SDK applies the bundled configuration, then fetches the remote one. It never refreshes the configuration more often than every 300 seconds. For how configurations are signed and checked, see Signed configuration.
Options
Section titled “Options”GronaOptions is a struct. Pass the parameters you need in this order; every parameter except workspaceKey and configKeys has a default.
| Parameter | Default | Validation | What it does |
|---|---|---|---|
workspaceKey |
Required | Matches [A-Za-z0-9._:-]{1,128} |
Your workspace key, for example acme-market. It is a public identifier, not a secret. |
configURL |
nil |
https only |
Signed remote configuration. Without it the SDK runs on the bundled copy only. |
bundledConfig |
nil |
File URL of the signed bundled configuration, applied at launch and offline, for example Bundle.main.url(forResource: "grona-config", withExtension: "json"). |
|
configKeys |
Required | At least one | Trusted configuration signing keys, as key id to public key: the base64 DER SubjectPublicKeyInfo of a P-256 key. Add the current and the next key. |
grantKeys |
[:] |
Trusted grant signing keys, in the same format. Needed for offers. | |
eventsURL |
nil |
https only |
Events endpoint. Without it the SDK sends no events and keeps no event queue. Debug builds keep the last 200 events in memory for the debug view. |
previewURL |
nil |
https only |
Preview pairing endpoint. Without it preview links can only clear pins. |
activationSource |
nil |
A GronaActivationSource that can switch campaigns off. See Activation source. |
|
grantSource |
nil |
A GronaGrantSource that fetches grants from your backend. See Offers and grants. |
|
theme |
GronaTheme() |
Fonts, colours and corner radius for Grona components. See Theming. | |
logger |
nil |
Receives SDK diagnostics. Silent when nil. See Test and debug. |
|
maxAddQuantity |
5 |
1 to 20 | Highest quantity one Grona action may add. See Handle actions. |
maxAddLines |
5 |
1 to 20 | Most lines one Grona action may add. |
sendPreConsentEvents |
false |
Send events buffered before consent once consent is granted. See Consent. | |
demoMode |
false |
Demos only, never in production. See Offers and grants. |
GronaOptionsError has four cases:
| Case | Thrown when |
|---|---|
.invalidWorkspaceKey |
The workspace key does not match [A-Za-z0-9._:-]{1,128}. |
.insecureURL(URL) |
configURL, eventsURL or previewURL has a scheme other than https. |
.missingConfigKey |
configKeys is empty. |
.invalidAddCap |
maxAddQuantity or maxAddLines is outside 1 to 20. |
Implement GronaHost
Section titled “Implement GronaHost”GronaHost is how Grona works through your app’s own code. The SDK never reaches into your app any other way. It is a main-actor protocol with four methods:
import Grona
@MainActorfinal class AcmeGronaHost: GronaHost { private let services: AcmeServices
init(services: AcmeServices) { self.services = services }
// A fast in-memory lookup in your catalogue; nil when the product is unknown func product(id: String) -> GronaProduct? { services.catalog.gronaProduct(id: id) }
// Add lines to the cart after the shopper tapped a Grona call to action func add(_ lines: [GronaLine], source: GronaSource) -> GronaAddResult { services.cart.addFromGrona(lines) }
// Swap one cart line, for example an out-of-stock item for its substitute func replace(productID: String, with line: GronaLine, source: GronaSource) -> GronaAddResult { services.cart.replaceFromGrona(productID: productID, with: line) }
// Open the route with your own router; false when the route is unknown func navigate(to route: GronaRoute, source: GronaSource) -> Bool { services.router.open(route) }}Here AcmeServices, catalog, cart and router stand for your app’s own types. Handle actions has a complete implementation and the rules for each return value.
Activation source
Section titled “Activation source”An activation source lets an external campaign system switch Grona campaigns off. Each campaign can list external ids, such as CMP-1024. When your source reports which external ids are active, a campaign that lists ids stays active only while at least one of them is active. Activation can only switch campaigns off, never on, and campaigns without external ids ignore it.
import Grona
struct AcmeActivationSource: GronaActivationSource { let campaigns: AcmeCampaignClient
// Called off the main actor. After 10 seconds the SDK stops waiting and cancels the task. func activeExternalIDs() async -> Set<String>? { do { return try await campaigns.fetchActiveIDs() // for example ["CMP-1024"] } catch { return nil // unknown: Grona does not narrow anything } }}Pass it as activationSource: AcmeActivationSource(campaigns: campaignClient) in your options.
- Return
nilwhen you do not know. Unknown never switches a campaign off. - The SDK polls the source every 5 minutes while the app is in the foreground. An answer is trusted for at most 10 minutes. A poll that fails, times out or finds the previous call still running makes activation unknown at once.
- After 10 seconds the SDK stops waiting, treats the answer as unknown and cancels the task. Check for cancellation, or use APIs that do, such as
URLSession’s async methods. The SDK calls the source again only once the earlier call has returned. - The protocol requires
Sendable. The SDK calls it off the main actor, sometimes while your own code runs, so keep it thread-safe and away from your views.
For how activation fits the kill switch, see Campaign activation and kill switch.
Refresh on demand
Section titled “Refresh on demand”The SDK refreshes on its own. When you need a refresh now, for example after a pull to refresh, call one of these:
Grona.refresh() // no completion signal
Grona.refresh { // Main actor, called once, also on timeouts and failures}In async code, use the async form:
await Grona.refresh()All three poll the activation source and ask for a configuration fetch, subject to the 300-second floor. The completion and async forms wait for an activation answer requested after the call (10-second timeout), then for the fetch and re-evaluation, before the completion runs or the async call returns. In an async context Swift chooses the async form for Grona.refresh(), so write await. To start a refresh from async code without waiting, use the completion form.
A campaign that was switched off stays off for the rest of the screen visit, even after it is switched back on. To show it at once, end the visit after the refresh and bind again: cancel the decision’s GronaSubscription, or set the slot’s screen to nil and back. See Screen visits for UIKit and SwiftUI code.
Next steps
Section titled “Next steps”- Identify shoppers when they sign in.
- Set consent from your consent flow.
- Push the basket and context.