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

iOS API reference

This page lists the public API of the Grona iOS SDK 0.1.0. The Grona product is the module Grona; the GronaDebug product is the module GronaDebug. Every type is public, and the value types are Sendable.

The SDK never throws into your app, with one exception: the GronaOptions initialiser throws GronaOptionsError on invalid input.

API Isolation Notes
Grona.start, Grona.decision, GronaSlotView, GronaDecision actions, Grona.presentDebugView Main actor Swift checks at compile time that you call them on the main actor.
Every other Grona method None Safe from any thread. Work runs off the main thread, and calls from any one thread are applied in the order you make them.
GronaHost Main actor The SDK calls it on the main actor only.
GronaGrantSource, GronaActivationSource, GronaLogger None, Sendable The SDK calls them off the main actor.
Decision closures, refresh completions Main actor Never called synchronously from the call that registers them.
public enum Grona

The SDK entry point. Call start once at launch. Every other call does nothing until then.

Member Description
static let version: String The SDK version, "0.1.0". Compared against the minimum SDK version in configurations.
@MainActor static func start(options: GronaOptions, host: any GronaHost) Starts the SDK. Constant time: work is queued off the main thread. Ignored on repeat calls and in app extensions. Keeps a strong reference to host for the life of the process.
static func setConsent(_ consent: GronaConsent) Sets the consent state. The SDK starts in .unknown on every launch: events stay in memory and none are sent.
static func identify(_ customerID: String?) An opaque internal customer id matching [A-Za-z0-9._:-]{1,128}, never a phone number, email or national id; nil on logout. A new id clears the profile and grants.
static func setProfile(_ profile: GronaProfile?) Profile from your own authenticated session. Ignored unless it matches the identified customer. nil clears it.
static func setContext(_ context: GronaContext) Sets the shopper context that rules read.
static func setBasket(_ basket: GronaBasket) Push a fresh snapshot whenever the basket changes. Live components and decisions re-evaluate.
static func track(_ event: GronaEvent) Records a commerce event.
@MainActor @discardableResult static func decision(for anchor: UIView, placement: String, screen: GronaScreen, key: String? = nil, onDecision: @escaping @MainActor (GronaDecision?) -> Void) -> GronaSubscription Decision for a host-rendered placement, bound to anchor. key is the line’s product id for keyed placements such as cart_line. onDecision runs on the main actor, never synchronously, and again only when the result changes. Binding the same anchor again replaces the previous subscription.
static func activeTreatments() -> [GronaTreatment] Treatments currently shown to this shopper in live slots, each with the grant token your order backend must verify.
static func refresh() Polls the activation source and asks for a configuration refresh, subject to the 300-second floor. No completion signal.
static func refresh(completion: @escaping @MainActor @Sendable () -> Void) Refreshes now and calls completion once on the main actor when done, also on timeouts, failures and before start.
static func refresh() async As above, returning when done. In an async context, Grona.refresh() resolves to this form.
@discardableResult static func handle(_ url: URL) -> Bool Handles a preview link: <scheme>://grona-preview?... or https://<host>/grona-preview?.... Returns true when the link was Grona’s, whether or not it was accepted.
static func reset() Logout or erasure: new install id, and the queue, customer, profile, grants, basket snapshot, preview pins and session cleared.
public struct GronaOptions: Sendable {
public init(
workspaceKey: String,
configURL: URL? = nil,
bundledConfig: URL? = nil,
configKeys: [String: String],
grantKeys: [String: String] = [:],
eventsURL: URL? = nil,
previewURL: URL? = nil,
activationSource: (any GronaActivationSource)? = nil,
grantSource: (any GronaGrantSource)? = nil,
theme: GronaTheme = GronaTheme(),
logger: (any GronaLogger)? = nil,
maxAddQuantity: Int = 5,
maxAddLines: Int = 5,
sendPreConsentEvents: Bool = false,
demoMode: Bool = false
) throws
}

Options for Grona.start. Each parameter is also readable as a let property of the same name.

Parameter Description
workspaceKey The workspace key, a public identifier, not a secret. Must match [A-Za-z0-9._:-]{1,128}.
configURL Signed remote configuration (https). Without it the SDK runs on the bundled copy.
bundledConfig File URL of the signed bundled configuration, applied at launch and offline.
configKeys Trusted configuration signing keys, key id to base64 DER SubjectPublicKeyInfo of a P-256 key. Add current and next. At least one is required.
grantKeys Trusted grant signing keys, in the same format.
eventsURL Events endpoint (https). 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 Preview pairing endpoint (https). Without it preview links only clear pins.
activationSource Source of on and off answers from an external campaign system.
grantSource Fetches treatment grants from your backend.
theme Visual tokens for SDK components.
logger Receives SDK diagnostics. Silent when nil.
maxAddQuantity Highest quantity one action may add, 1 to 20. Default 5.
maxAddLines Most lines one action may add, 1 to 20. Default 5.
sendPreConsentEvents Send events buffered before consent once it is granted. Default false.
demoMode Demo only, never in production. Takes effect only when the signed configuration also enables demo mode. Default false.
public enum GronaOptionsError: Error, Equatable, Sendable
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.
@MainActor
public protocol GronaHost

Implemented by your app. The SDK calls it on the main actor only and never reaches into app code.

Requirement Description
func product(id: String) -> GronaProduct? Product facts for validation and copy. Must be a fast in-memory lookup; nil when unknown.
func add(_ lines: [GronaLine], source: GronaSource) -> GronaAddResult Adds lines after the shopper tapped a Grona call to action, or your code called a decision action.
func replace(productID: String, with line: GronaLine, source: GronaSource) -> GronaAddResult Replaces one basket line, for example an out-of-stock item with its substitute.
func navigate(to route: GronaRoute, source: GronaSource) -> Bool Opens route with your own router; false when the route is unknown or cannot open.
@MainActor
public final class GronaSlotView: UIView

A place in your UIKit layout where Grona renders native components: banner, progress, offer and message. With nothing to show it stays laid out at zero height. Never hide it.

Member Description
init(placement: String) Creates a slot for placement.
override init(frame: CGRect) Creates a slot with an empty placement. Set placement before screen.
required init?(coder: NSCoder) For storyboards and nibs. Set placement in the Attributes inspector.
@IBInspectable var placement: String The placement. Set it before screen.
var screen: GronaScreen? Starts the slot; rules read it. An equal value continues the visit; nil ends it and empties the slot.

The slot changes its height through Auto Layout when content arrives or leaves. In table and collection views, set selfSizingInvalidation to .enabledIncludingConstraints.

public struct GronaSlot: View {
public init(placement: String, screen: GronaScreen?)
}
extension View {
public func gronaDecision(
placement: String,
screen: GronaScreen?,
key: String? = nil,
onDecision: @escaping @MainActor (GronaDecision?) -> Void
) -> some View
}
Member Description
GronaSlot(placement:screen:) A GronaSlotView for SwiftUI. Reports its height to SwiftUI layout. A nil screen ends the visit and empties the slot.
gronaDecision(placement:screen:key:onDecision:) Requests a decision with the modified view as the anchor, measured through a UIKit view placed behind it. Calls the latest onDecision you pass. A nil screen ends the subscription and its visit; a new placement, screen or key resubscribes.
public struct GronaScreen: Hashable, Sendable

Where the shopper is. Rules read it.

Member Description
let type: String The screen type, such as home or product.
let productID: String? The product, for product screens.
let categoryID: String? The category, for category screens.
let treatmentTypes: [String] For order confirmation: treatments your order backend applied.
let name: String? Your name for an other screen.
static let home, categories, search, cart, checkout Fixed screens.
static func category(_ categoryID: String) -> GronaScreen A category listing.
static func product(_ productID: String) -> GronaScreen A product page.
static func orderConfirmation(treatmentTypes: [String]) -> GronaScreen Order confirmation. treatmentTypes are the treatments your order backend actually applied.
static func other(_ name: String) -> GronaScreen Any other screen.
public struct GronaBasket: Sendable

Basket snapshot pushed with Grona.setBasket. Money is in minor units.

Member Description
init(currency: String, lines: [GronaBasket.Line] = [], totalMinor: Int? = nil) The currency code, such as USD, the lines, and the total rules compare against. totalMinor defaults to the sum of the lines.
static func empty(currency: String) -> GronaBasket An empty basket.
let currency: String, let lines: [GronaBasket.Line], let totalMinor: Int The values passed, with totalMinor computed when it was nil.
GronaBasket.Line(productID: String, quantity: Int, unitPriceMinor: Int, inStock: Bool) One basket line. Each value is readable as a property.
public struct GronaContext: Sendable

Shopper context that rules can read. Signed-in state comes from Grona.identify.

Member Description
init(locale: String = "en", branchID: String? = nil, deliveryType: String? = nil) The shopper’s locale, your store or branch id, and your delivery type. Each value is readable as a property.
public struct GronaProfile: Sendable

Per-shopper data from your authenticated session, bound to one customer id. Segment ids are opaque.

Member Description
init(customerID: String, segments: Set<String> = [], lists: [String: [String]] = [:], attributes: [String: String] = [:]) The customer the profile belongs to, segment ids, named lists of product ids, and string attributes. Each value is readable as a property.
public struct GronaEvent: Sendable

Commerce events your app forwards from its analytics layer. Money is in minor units; no search text, no full baskets. Exception: on keyed placements such as cart_line, campaign events carry the line’s product id as their key, in every arm. So while a cart_line campaign is live, the product ids of the cart lines bound to it leave the device.

Member Description
static func viewItem(productID: String, priceMinor: Int) -> GronaEvent A product view.
static func addToCart(productID: String, quantity: Int, priceMinor: Int) -> GronaEvent A cart add. priceMinor is the unit price.
static func removeFromCart(productID: String, quantity: Int) -> GronaEvent A cart removal.
static func beginCheckout(totalMinor: Int) -> GronaEvent Checkout started.
static func purchase(orderID: String, totalMinor: Int, productIDs: [String], treatments: [GronaTreatment]) -> GronaEvent A confirmed order. productIDs is the whole order; the SDK keeps only those Grona campaigns showed or added.
public struct GronaDecision: Equatable, Sendable

A product decision your app renders with its own views. Treat unknown types as no decision. Two values are equal only when they come from the same delivery. A decision delivered again, for example after a rebind, is a new value even when its content is the same, so SwiftUI redraws with its new action handle. Do not compare decisions field by field to skip updates.

Member Description
let placement: String The placement.
let campaignID: String The campaign.
let variantID: String The variant.
let type: GronaDecisionType .rail, .bundle, .productCard, .substitute, or a type from a newer configuration.
let title: String?, let subtitle: String?, let badge: String?, let ctaLabel: String? Copy from the configuration. Render as plain text.
let products: [GronaDecisionProduct] The products to show. Never empty.
let key: String? For keyed placements such as cart_line: the line this decision belongs to.
let extras: [String: String] Layout hints from the configuration, such as position for feed rows. Untrusted.
let isPreview: Bool true while a preview pin produced this decision.
var source: GronaSource Placement, campaign and variant.
@MainActor @discardableResult func add(_ productID: String, quantity: Int = 1) -> GronaAddResult Call from the shopper’s tap. The SDK validates, logs the action and calls GronaHost.add.
@MainActor @discardableResult func addAll() -> GronaAddResult Adds every product in the decision.
@MainActor @discardableResult func replace(with productID: String) -> GronaAddResult Replaces key with productID through GronaHost.replace.
@MainActor func dismiss() Hides this decision for the rest of the session.
public struct GronaDecisionType: RawRepresentable, Hashable, Sendable
Member Description
init(rawValue: String), let rawValue: String The type as it appears in the configuration.
static let rail, bundle, productCard, substitute The types in this version: "rail", "bundle", "product_card", "substitute".
public struct GronaDecisionProduct: Hashable, Sendable
Member Description
let productID: String The product.
let quantity: Int The quantity addAll() adds.
let label: String? A per-product label from the configuration.
public final class GronaSubscription: Sendable

The handle Grona.decision returns. The anchor keeps the subscription alive, so releasing the handle does not cancel it.

Member Description
func cancel() Ends the subscription and its screen visit. Safe from any thread.
public struct GronaTreatment: Sendable, CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable

A money-bearing treatment. Display state only: your order backend applies it only after verifying token.

Member Description
let campaignID: String The campaign.
let variantID: String The variant.
let placement: String Where it was shown.
let type: String The treatment type.
let valueMilli: Int Value in thousandths, so 10000 is 10 (10% off with percent_off).
let token: String? Grona-signed grant (compact JWS); nil in demo mode. A real backend ignores a treatment without a token: no treatment, and the order still goes through.
let isDemo: Bool true for a demo-mode treatment.
var description: String, var debugDescription: String, var customMirror: Mirror A summary that leaves the token out, so printing or dumping a treatment never writes the grant to your logs.
Type Members Description
enum GronaConsent: Sendable unknown, granted, denied The shopper’s consent state.
enum GronaAddResult: Sendable accepted, partial, rejected The outcome of an add or replace.
struct GronaLine: Hashable, Sendable init(productID: String, quantity: Int = 1), let productID: String, let quantity: Int One product and quantity in an action.
struct GronaSource: Hashable, Sendable let placement: String, let campaignID: String, let variantID: String Which campaign and variant an action came from.
struct GronaRoute: Hashable, Sendable let type: GronaRouteType, let id: String? A typed destination your app resolves itself. Grona never passes raw links.
struct GronaRouteType: RawRepresentable, Hashable, Sendable init(rawValue: String), let rawValue: String; static let home, product, category, cart, checkout, deals Route types. Unknown types may arrive from newer configurations; return false from GronaHost.navigate(to:source:) for them.
public struct GronaProduct: Sendable

Product facts from your app. Money is in minor units.

Member Description
init(id: String, name: String, priceMinor: Int, wasPriceMinor: Int? = nil, imageURL: URL? = nil, inStock: Bool = true) Id, display name, price, a previous price, the product image, and whether it can be added. Each value is readable as a property.
public struct GronaTheme: Sendable

Visual tokens so SDK components match your app. Each value is readable as a property. Colours are resolved once, the first time Grona draws a component.

Parameter Default
regularFont: UIFont? nil, the system font
boldFont: UIFont? nil, the bold version of the regular font
primary: UIColor #054B3A
onPrimary: UIColor #FFFFFF
accent: UIColor #6FCF97
surface: UIColor #FFFFFF
text: UIColor #1A1C1A
mutedText: UIColor #6B706C
cornerRadius: CGFloat 14, in points, kept between 0 and 32

See Theming for what each token colours.

Type Requirement Description
protocol GronaLogger: Sendable func log(_ level: GronaLogLevel, _ message: String) Receives SDK diagnostics, called off the main actor. Never given customer ids, baskets, tokens or keys.
enum GronaLogLevel: Int, Comparable, Sendable debug, info, warning, error The level of a log message.
protocol GronaActivationSource: Sendable func activeExternalIDs() async -> Set<String>? On and off answers from an external campaign system, called off the main actor. nil when unknown. After 10 seconds the SDK stops waiting and cancels the task. Activation can only switch campaigns off.
protocol GronaGrantSource: Sendable func fetchGrants(customerID: String) async throws -> [String] Fetches grants from your backend, which asks Grona’s server for them. Called off the main actor. Returns compact JWS grants, or throws on failure. After 10 seconds the SDK stops waiting and cancels the task.

The GronaDebug module. Import it in debug builds only.

Member Description
@MainActor static func Grona.presentDebugView(from viewController: UIViewController) Presents the debug view modally from viewController. Does nothing, and logs a warning, unless GronaDebug was compiled with the DEBUG condition.
struct GronaDebugView: View with init() The debug view for SwiftUI, for a sheet or a navigation destination. Empty unless GronaDebug was compiled with DEBUG.