SDKsiOS

Changelog

Version history and release notes for the iOS SDK.

Version 1.0.0 (September 15, 2026)

1.0.0 is now stable and no longer in beta. Update your dependency to the stable release. Upgrading from 0.x? Follow the migration guide.

Version 1.0.0-beta.1 (August 25, 2026)

First release of the 1.0 line — the API the SDK stabilizes on for GA — published as a beta on Swift Package Manager; pin the exact version. The full 0.x → 1.0 upgrade path is in the migration guide.

Highlights

  • Impressions now require the ad to be on screen. An impression is billed only once the card has rendered and at least one visible pixel of it is inside the host window — surviving any ancestor that clips — instead of at first layout, which billed cards laid out below the fold. Expect impression counts to step down (and CTR per impression to step up) in layouts where ads render off screen or clipped: feeds, LazyVStacks, long transcripts. The separate ad_visible viewability signal (50% visible for 1 second) reads the same clip-aware geometry. No integration changes.
  • Every request now sends a real User-Agent, so device and OS classification is accurate downstream — macOS hosts are no longer misclassified as iOS phones, and iPad now reports as a tablet.
  • Expanded privacy manifest. PrivacyInfo.xcprivacy now also declares Coarse Location and Other Diagnostic Data, and adds the Third-Party Advertising purpose to the existing declarations; NSPrivacyTracking stays false. Review your App Privacy answers when you upgrade — see data collection.
  • Event reporting is stamped at the interaction. Each event now carries a unique id and the moment the tap, view, or dismissal actually occurred, so analytics measure the user rather than the network hop.
  • Under-13 age values are rejected. A stored under-13 age range is discarded and purged rather than sent — see Children's privacy.

Breaking changes

  • The legacy V1 query surface is removed: Koah.shared.prefetch(query:queryResponse:messageId:conversationId:), resolveAd(query:queryResponse:messageId:conversationId:), and cancelPrefetch(messageId:). Use prefetchAd(adContext:cacheKey:), requestAd(adContext:cacheKey:), and cancelPrefetchAd(cacheKey:); every ad fetch now goes through the unified V2 path.
  • KoahAdLoader is removed (deprecated in 0.10.0). Pair prefetchAd(adContext:cacheKey:) with KoahCard(cacheKey:) instead.
  • The deprecated KoahCard(adContext:overrideMaxWidth:overrideMaxHeight:cacheKey:) initializer is removed. Bound slots per axis with width / height (KoahAxisSize).

Version 0.10.0 (August 12, 2026)

Highlights

  • Koah's servers can now pick which small image-card treatment a fill renders: today's image-leading control, a labelled action button, a trailing square image, or a content-first layout with a full-height 1.91:1 creative, inline advertiser identity, one headline, and the existing chevron. The selection arrives with the ad; anything this SDK doesn't recognise renders the control. Every fill keeps rendering today's image-leading control until the selection is turned on server-side, so this release changes nothing on its own. Your integration is unchanged: no new format names appear in the ad's format, no new content fields were added, and the SDK gains no new dependency, configuration, or setup step.
  • Demographic targeting: gender and household income. Call Koah.shared.setGender(.female) and Koah.shared.setIncomeBracket(.income50kTo100k) (brackets are annual household income in thousands of USD). Values are persisted locally and uploaded for ad targeting, joining the existing setAge / setAgeRange. Pass .unknown to stop this device storing and re-sending a value — a local clear only; it does not delete a value already sent to Koah.

Improvements

  • KoahEvent.impressionTracked now fires only when the impression was accepted by Koah. Previously it fired on every render regardless of the outcome, so a transient failure reported an impression Koah had not recorded. Publishers who count this callback should expect their totals to line up with Koah's reporting rather than with render counts. Matches Android, Flutter and React Native.
  • Rebuilt impression visibility tracking. The tracker now actively resamples while a card is on screen and resets its dwell clock when the card drops below the visibility threshold, so fast scrolls, covered cards, and half-visible slots can no longer accrue toward an impression they shouldn't.
  • Fixed a first-configure crash and a class of init-order failures. A blank publisherId is now rejected only on the first configure; a duplicate call — even one carrying a transiently blank id — warns and no-ops instead of taking down a host that was already correctly configured. setAge no longer crashes on out-of-range values, and age ranges are transmitted correctly.
  • card_text_md no longer de-renders under large Dynamic Type. Custom-font rendering is pinned to SDK-locked sizes, so accessibility text settings can't push a text card past its budget and blank the slot.
  • Slot-layout fixes for lazy containers. Cards inside LazyVStack no longer starve their fetch, and transient zero-size layout passes no longer emit a spurious slotTooSmall.
  • Expandables are never bricked and scroll properly. A failed expanded-content fetch now retries on the next tap and falls back to the CTA link; the expanded body scrolls within height-capped slots and fades its bottom edge until fully scrolled.
  • Image cards recover from zero-width first layouts. The card_image_md hero image re-arms its load when the card's width changes, so a zero-width first pass can no longer latch the hero grey.
  • Small-card layout: advertiser icon leads, ad image trails. The small card now leads with the advertiser icon for clearer sponsorship attribution, with the creative thumbnail at the trailing edge (matches Android).
  • ad_clicked events now carry tap position and viewport geometry (tap point, ad frame, window size, and display scale, all in points), improving click-quality signals with no integration changes.
  • Malformed fills with null image metadata now render gracefully instead of being dropped.
  • KoahAdLoader is deprecated. Pair Koah.prefetchAd(adContext:cacheKey:) with KoahCard(cacheKey:) instead.
  • Removed the unreleased Category Rail and Photo Stack formats before any stable release shipped them.

API changes

⚠️ Removed

  • The server-decoded ad-content model is no longer public (KoahAd, KoahAdvertiser, KoahProduct, wire payloads, …) — the supported path returns KoahAdResult and renders via KoahCard/components, with the public KoahSampleAd family replacing raw-model fixtures. Breaking.
  • KoahAdSizeConstraints — internal render-envelope constants, now internal; the public sizing contract is the slot-bounds model. Breaking.
  • AdSource and the source: parameter on requestAd/prefetchAd — public requests always used the direct path; the parameter is gone. Breaking.
  • KoahError — the branded error enum is gone; the only error a normal integrator receives is KoahAdResult.error(message:fromCache:), and platform-native error idioms are used elsewhere. Breaking.
  • Koah.clearCache() / Koah.isInitialized — now internal. Breaking.
  • Category Rail and Photo Stack — the unreleased preview APIs, wire fields, and renderers were withdrawn before their first stable release; released consumers lose no API.

⚠️ Changed

  • The size/capability API is goneKoahAdSize, KoahAdOptions, and every size-taking requestAd/prefetchAd/card overload are removed; pass slot dimensions (or omit for an unbounded slot). Breaking.
  • KoahCard sizing is per-axis — new width/height parameters of type KoahAxisSize (.fixed, .range(min:max:), .fill; nil = unbounded) replace overrideMaxWidth/overrideMaxHeight (the old initializer remains with a deprecation warning), and card_text_md gains a text-length budget so over-long copy de-renders instead of clipping. Breaking.
  • KoahAdResult is a discriminated union.filled(fromCache:) / .noFill(fromCache:) / .error(message:fromCache:) replace the old filled flag, so success, no-fill, and failure are distinguishable; the result no longer carries format. Breaking.
  • Logging config is logLevel: KoahLogLevel — the debug: Bool flag is replaced by one shared enum (.none/.error/.warning/.info/.debug; .none is the default). Breaking.
  • Preview requests are requestPreviewAd(previewFormat:cacheKey:) — renamed from the requestAd(previewFormat:) overload; the context-based requestAd is unchanged. Breaking.
  • reset() is no longer public — teardown is unified and internal on all four SDKs. Breaking.

Deprecated

  • KoahAdLoader — pair prefetchAd(adContext:cacheKey:) with KoahCard(cacheKey:) instead. (Removed in 1.0.)

Added

  • KoahSampleAd family — hand-constructable public specimens (KoahSampleAd, advertiser/product/image/poll sub-types, ready-made .card/.image/.poll/.expandable) for previews, demo galleries, and snapshot fixtures without a network fetch.
  • KoahEvent.inAppBrowserClosed — the in-app-browser dismissal, already tracked internally, now reaches the consumer onEvent callback (parity with Android).
  • setGender(_:) / setIncomeBracket(_:) — publisher-provided demographic targeting alongside the existing age dimension.

Version 0.9.1 (June 23, 2026)

Highlights

  • Ad disclaimers. When an ad carries disclaimer text, KoahCard now shows an info (ⓘ) affordance next to the "Sponsored" label that opens a disclaimer sheet. Surfaced on the model as the new optional KoahAd.disclaimer; the affordance renders automatically with no integration changes.

Improvements

  • Advertiser icons render more gracefully. Icons now show a skeleton while loading, crossfade in on success, and fall back to a vector placeholder mark when there's no icon URL or the image fails to load.
  • /client/info uploads are debounced. Device info is now uploaded only when a stable fingerprint changes since the last successful send, instead of on every launch — fewer redundant network calls.

Version 0.9.0 (June 4, 2026)

Highlights

  • Pixel-precise ad sizing with KoahDimensions. requestAd / prefetchAd and KoahCard now take explicit slot constraints — pass overrideMaxWidth / overrideMaxHeight (in points) on KoahCard, or a KoahDimensions(maxWidth:maxHeight:) value on the request, to bound an ad to your layout. Each axis is independent; omit both for an unbounded slot. Replaces the coarse t-shirt-size buckets with exact dimensions.

Improvements

  • size deprecated in favor of dimensions. The KoahAdSize t-shirt-bucket size: parameter on requestAd / prefetchAd now emits a deprecation warning. Migrate to overrideMaxWidth / overrideMaxHeight on KoahCard (or KoahDimensions on the request); omit for unbounded. size will be removed in a future release.

Version 0.8.1 (June 2, 2026)

Improvements

  • Unified impression & visibility tracking. Reworked internal visibility bookkeeping so impression and viewed events follow the same gating semantics across iOS, Android, Flutter, and React Native. Event timing for partially-occluded or de-rendered cards is now consistent with the other SDKs.

Version 0.8.0 (May 29, 2026)

Highlights

  • Ad report flow. Tapping the new button on KoahCard's caption opens a half-sheet (About this adReport this ad with five reasons + Submit) that posts to POST /k/report. No new public API surface and no opt-in — the affordance ships on every standard KoahCard. Sheet auto-adapts to system light/dark via the ambient KoahTheme and stays iOS 13+ compatible.
  • Optional slot parameter. Koah.shared.requestAd(adContext:size:cacheKey:slot:) and prefetchAd(...) now accept an optional slot: String? that surfaces in matching and reporting. Lets a single page tag multiple distinct ad placements without colliding on the cache key or muddying analytics.
  • requestPreviewAd(previewFormat:) overload. New Koah.shared.requestPreviewAd(previewFormat:) returns a deterministic sample creative for a given format (.small / .medium / .expandable / .poll), so publishers can wire their preview UIs without depending on live fill.
  • Optional experimentTag. New experimentTag: String? parameter on requestAd / prefetchAd is forwarded with every ad request, letting publishers split-test creatives or surface variants without piggy-backing on the cache key.
  • Poll CTA with animated reveal. Clickable poll CTA reveals the post-vote state with a word-by-word blur-reveal animation gated on 50% visibility, matching the expandable format's reveal pattern.

Improvements

  • adText removed from publisher-facing fill events. Tracking events no longer leak ad copy to the publisher's analytics surface.

Version 0.7.1 (May 18, 2026)

Bug Fixes

  • Ad images now render on the V2 endpoint. KoahAd was decoding the image off a JSON key the V2 /ad_requests server response doesn't emit, so image-bearing creatives rendered as text-only. The wire mapping is fixed and any ad with a KoahResponsiveImage payload now displays the image inside KoahCard.

Version 0.7.0 (May 12, 2026)

Highlights

  • Unified requestAd API (v2). New Koah.shared.requestAd(adContext:size:cacheKey:), Koah.shared.prefetchAd(adContext:size:cacheKey:), and Koah.shared.cancelPrefetchAd(cacheKey:), plus a public KoahAdContext enum (.conversation / .article / .feed / .static) and a KoahAdResult(filled, fromCache, format) return type. Replaces the per-surface prefetch / resolveAd primitives with a discriminated context the server uses for matching, copy selection, and format eligibility — same shape as Android, React Native, and the upcoming Flutter SDK. Migration: Koah.shared.prefetch / resolveAd / cancelPrefetch still work but emit deprecation warnings; new code should adopt requestAd / prefetchAd.
  • KoahCard(adContext:size:cacheKey:) initializer (breaking). Replaces the primary KoahCard(query:queryResponse:messageId:isResponseReady:) initializer. messageId: is renamed to cacheKey: across all initializers. Cache-warmed cards now fire onEvent(.adServed) consistently — analytics no longer under-count serves that came from a prior prefetchAd.
  • KoahFont styling type (breaking). KoahTheme.fontFamily: Font? is replaced with KoahTheme.font: KoahFont?. Callers get explicit control over typeface, weight, and Font.Design while the SDK locks per-element sizes. Use KoahFont.system(weight:design:) or KoahFont.custom(_:weight:design:). Migration: fontFamily: yourFontfont: .custom("YourFont", weight: .regular).
  • Expiry-aware ad cache. Cached entries honor the server's expires_at, evicted on read once stale, so a long-lived Koah instance doesn't keep replaying ads past their expiry.

Improvements

  • KoahFetcher logging. Malformed /ad_requests responses log as Malformed /ad_requests response: … instead of being lumped in with transport failures, so server-shape regressions surface in debug builds instead of looking like no-fills.

Version 0.6.0 (April 2026)

Highlights

  • New KoahTheme styling API (breaking). Replaces the ~17-knob KoahCardStyle with a minimal 4-knob KoahTheme (colorScheme, accentColor, fontFamily, cornerRadius) applied once at the root via .koahTheme(_:) and propagated through SwiftUI's @Environment. Auto dark mode flips with the system color scheme; text-on-accent is auto-contrasted via WCAG luminance. Mirrors the Android KoahTheme refactor. Migration: KoahCardStyle, KoahCardOptions, KoahCardSize, KoahCardSizeConstraints, and KoahCardWidthBehavior are removed — callers move to .koahTheme(...) + KoahAdOptions(size:), with width controlled via SwiftUI's native .frame(...). A new chrome: .bare parameter on KoahCard provides the borderless variant.
  • Prefetch / resolveAd primitives for feeds. New Koah.prefetch, Koah.resolveAd, Koah.cancelPrefetch, KoahAdState, KoahAdLoader, and a KoahCard(messageId:) initializer let SwiftUI feed integrations resolve ad state before rendering. (Superseded by the v2 requestAd API in 0.7.0.)
  • Medium card size enforcement. KoahCardSizeConstraints (220–1000 width, 280–1000 height) is now enforced at measure time via the new KoahMinSizeProbe SwiftUI Layout. Below minWidth, or when content would clip below minHeight, the card collapses to zero size and logs once. iOS 16+ only; older OS versions fall through as a no-op.

Improvements

  • Debug warning on no-fill with a production key. Debug builds now log a loud, unsilenceable warning when the server returns no-fill on a non-demo publisher key, pointing integrators at demo keys for reliable local testing.
  • Expanded /client/info payload. The SDK now sends deviceModel, ipAddress, userAgent, and isLocationEnabled. Locale normalized from underscore to hyphen format (en-US) to match Android and Flutter.

Version 0.5.2

Bug Fixes

  • Fixed URLSession blocked by iOS tracker protections. The SDK's privacy manifest previously declared app.koah.ai as an NSPrivacyTrackingDomain with NSPrivacyTracking: true, causing iOS 17+ to hard-block all Koah API calls under Limit IP Address Tracking (default ON). The declaration did not match actual behavior — Koah's backend doesn't correlate device IDs across publisher apps and the SDK never reads IDFA. The manifest has been corrected: NSPrivacyTracking and per-data-type NSPrivacyCollectedDataTypeTracking are now false, and NSPrivacyTrackingDomains has been removed.

Version 0.5.1

Bug Fixes

  • Fixed small cards never rendering. KoahCardOptions.size is now forwarded to the server on /query. Previously the SDK only used size for a client-side strict-match after the fetch, so the server always returned a medium ad and small-slot requests were dropped as no-fill.

Version 0.5.0

Highlights

  • Compact small card format. A new KoahCardSize option on KoahCardOptions lets integrators render the compact "small" card variant, matching the format already shipping on Android and web. Includes a custom marquee treatment with edge fade and reduced-motion fallback.
  • Age range visitor attributes. Publishers can now pass demographic signals for higher RPMs via the new Koah.setAgeRange(_:) and Koah.setAge(_:) APIs. Values persist across launches and auto-resend on SDK init.

Version 0.4.1

Bug Fixes

  • Fixed stale client ID when publisher ID changes. The SDK now validates that the cached client ID belongs to the current publisher before reusing it. Previously, switching publisher IDs (e.g. between environments) could send requests with the old publisher's client ID, causing attribution to the wrong account.

Version 0.4.0

Highlights

  • New ad formats: Expandable and Poll. KoahExpandableCard shows a tappable card that expands inline to reveal richer ad content, and KoahPollCard renders an interactive poll the user can vote on directly. Both are first-class SwiftUI views that fit alongside KoahCard.
  • KoahCard visual refresh. Layout, spacing, typography, and color treatments now match the latest cross-platform design (parity with Android, Flutter, and React Native).
  • App lifecycle tracking. The SDK now reports foreground/background transitions via KoahTrackingEvent, matching the other platform SDKs.
  • macOS support. The XCFramework now ships a macOS slice in addition to iOS device and simulator.

Improvements

  • Reliable impression tracking. The visibility tracker has been rewritten with a UIKit-backed VisibilityProbe and a Combine pipeline that accounts for safe area insets, isHidden, and alpha. Impressions now fire correctly even when a card starts below the fold and is scrolled into view.
  • Unified version management. The SDK version is now sent in API request headers, letting the server segment behavior by SDK version.
Was this helpful?