Skip to content

Release Notes

Highlights and behavior changes per release. The complete history, binary checksums, and downloadable XCFrameworks live on GitHub Releases; the full changelog ships as CHANGELOG.md in the package.

Pin the SDK exactly and bump deliberately:

.package(url: "https://github.com/growlads/elo-ios-sdk", exact: "0.6.1"),
Latest GitHub release
  • Change: the card is set like a feed ad. A header row holds the brand mark beside the brand name with the disclosure under it, the description follows as body copy at the card’s full width (up to three lines, no scrolling), and the CTA pill sits on a row of its own at the trailing edge. The card is taller than before and its height depends on the copy, so a bounded slot now needs a maxHeight of at least 192 points to get it; shorter slots get the strip, as does the loading placeholder. EloAdStyle.descriptionOverflow now applies to the strip only. See Ad Formats.
  • New EloDebugAdOperation.tapSurface. The operation reports the ad’s presentation.tap_surface value (cta or full) next to the other presentation values; nil when not sent or on a no-fill.
Breaking changes GitHub release
  • Breaking Describe the slot, and the server chooses the format. EloAdLayout, the layout: parameters on EloAdView, and the .eloAdLayout(...) modifier are removed. Pass maxHeight, and optionally maxWidth, in points to Elo.loadAd, Elo.preloadAd, EloChatSession.loadAd, and EloAdView(messages:contextObjects:maxHeight:maxWidth:...). The server returns the format that fits: the card needs 80 points of height, and the strip fits a slot from 72 to 79 points. EloAdView renders whichever format the ad names, including through EloAdView(ad:) and EloAdView(result:). When no format fits, the result is a no-fill. Leaving maxHeight nil keeps the card you get today. The loading skeleton follows maxHeight the same way, and EloAdLoadingView(layout:) becomes EloAdLoadingView(maxHeight:). .eloKeyboardBannerAd requests the strip’s height, so it keeps receiving the strip with no change on your side. EloAdRenderConfiguration.layout is removed as well; use EloAdRenderConfiguration(style:sponsoredLabel:). See Ad Formats.
  • Change: a preloaded ad is served only to a load with the same maxHeight and maxWidth. The slot size is part of the preload cache key, so pass the same values to Elo.preloadAd and to the load that should use the ad.
  • Breaking EloDebugAdOperation.displayPosition is replaced. The operation now reports the chosen format (strip or card), the maxHeight and maxWidth the request sent, and the ad’s ctaEnabled, ctaVariant, and tapInset presentation values. The SDK no longer sends display_position on the ad request; the server derives it from the format it chose.
  • Breaking The manual tracking calls are removed. Elo.trackRender, Elo.trackImpression, Elo.trackClick, and the matching EloChatSession methods are no longer public. EloAdView sends render, impression, and click tracking itself, so render ads with EloAdView or .eloKeyboardBannerAd. EloAdEvent.didTrackImpression and EloAdEvent.didReceiveClick are removed with them; use EloAdDelegate for impression and click callbacks.
  • Breaking AdTracker is no longer public. EloAd.init drops its tracker: parameter and builds a display-only ad that sends no events. Remove the argument from previews and tests.
  • New Each message’s id and createdAt are sent to Elo. ChatMessage gains optional id and createdAt, and MessageRepresentable gains the same two properties with nil defaults. The SDK sends them with the message, so Elo knows when each turn happened and can connect a turn to a Search API call that sends the same id as X-Elo-Message-Id. Set them once, when the turn is created, and reuse them on every request; a .summary message should carry the id and time of the turn it summarizes. Without createdAt, Elo records the time it received the message. An id longer than 256 Unicode code points or containing non-printable characters, and a time before 2020 or more than a day ahead, are left out with a warning. Neither affects the preload cache. See ChatMessage.
  • New Report your own in-app browser. If you open ad clicks in your own browser, call Elo.trackBrowserOpened(_:) when it opens the ad’s click URL and Elo.trackBrowserClosed(_:) when it closes. The SDK sends a browser_open event, then a browser_close event with the time in between as dwell_ms, so Elo can measure how long the user stayed on the advertiser’s page. The time is measured on the device, includes time the app spent in the background, and is capped at 24 hours. A second open before the close, and a close without an open, send nothing. Elo-served ads only, and analytics only: never billed. Both events reach your debug observer as the new EloTrackingEvent.browserOpen and .browserClose cases; a switch over that enum without a default needs the extra cases. Declare the data as Product Interaction in your nutrition label if you have not already; see Your own in-app browser and Privacy & Consent.
  • New The SDK reports where the ad was on screen when it became viewable. Once an Elo-served ad has been at least 50% visible for one second, EloAdView posts one analytics event with the ad view’s size and position in the window, the window size, the visible fraction, and the time since render, so Elo can verify that ads render in the SDK’s own view. Nothing about the surrounding app is sent, and the impression is recorded separately. It reaches your debug observer as the new EloTrackingEvent.viewable case. Declare it as Product Interaction in your nutrition label; see Privacy & Consent.
  • Breaking The diagnostics snapshots are replaced by a debug event observer. Elo.Debug.snapshot(), Elo.mediationDebugSnapshot(), and Elo.setRequestPayloadCaptureEnabled(_:) are removed, along with the DiagnosticsSnapshot, DiagnosticsEntry, TrackingDiagnosticsEntry, TrackingTotals, IntegrationStatus, and EloMediationDebugSnapshot types. Elo.setDebugObserver(_:capturePayloads:) replaces all of them: the SDK reports each EloDebugEvent as it happens (load and preload outcomes, and tracking attempts and their outcomes) and keeps no history of its own. An app that wants a recent-loads list or tracking totals records the events itself. With no observer attached, the SDK stores no load history, no tracking history, and no request payloads. Pass capturePayloads: true to also receive the redacted request payload and the decoded ad response that setRequestPayloadCaptureEnabled(true) used to enable.
  • New The analytics body behind a tap. EloDebugEvent.trackingAttempted carries a payloadJSON, the analytics body the operation posted, so an integration can show on the device exactly what a tap reported. Only tap and browser events have a body: render and impression are URL pings, and click posts nothing. It stays nil unless the observer was registered with capturePayloads: true.
  • Breaking AdMob mediation and the client-side auction are removed. Elo is now the SDK’s only demand source. If you run another ad network, request Elo first and hand the slot to your other network on .noFill or .error in your own code; see Falling back to another network and Other Ad Networks. Removed: the EloAdsMediationAdMob product and AdMobNetworkAdapter; the AdNetworkAdapter, AdMediator, AdBid, AdBidRequest, AdConsent, and AdAdapterError types; EloConfiguration.adapters; the ValidationIssue.duplicateNetworkId, .missingInfoPlistKey, and .missingSKAdNetworkId cases; the auction and adapter debug events with their EloAdapterAuctionOutcome and EloAuctionOutcome payloads; EloDebugConfiguration.adapterIds; and EloErrorCategory.adapter. Changed: AdResult.loaded is now .loaded(EloAd, eCpm:) with no networkId; EloAdLoadOutcome.loaded is .loaded(eCpm:); NoFillReason.auctionTimeout is renamed .timeout; and EloDebugConfiguration.auctionTimeout is renamed requestTimeout. Releases ship a single EloAds.xcframework with no third-party dependencies.
  • Breaking The deprecated Elo.initialize(...) is removed. It only forwarded to configure, and it had been deprecated since 0.1.7. Use Elo.configure(publisherId:adUnitId:shareGeoLocation:geoLocationPrecision:), or build an EloConfiguration and call Elo.configure(with:) for COPPA, TFUA, logLevel, or baseUrl.
  • Breaking SKAdNetworkConversionHelper is removed. It wrapped Apple’s SKAdNetwork conversion APIs but no part of the SDK used it. Call SKAdNetwork directly.
  • New The SDK requests a creative’s device-side tracking URLs. Some demand sources count a render, impression, or click only when the device that showed the ad requests their URL. When a creative carries such URLs, the SDK requests them itself, at most once per showing. The requests carry none of the SDK’s headers or session data, and their outcome does not affect Elo’s own tracking or any callback your app receives. See Privacy & Consent.
  • Elo.sdkVersion moved. It behaves the same, but it now lives on Elo itself rather than under the removed Elo.Debug namespace.
  • New Theme the ad’s typeface, color scheme, and surface. EloAdStyle gains font (EloAdFont.system(design:) or .custom(_:), drawn at the SDK’s own sizes), colorScheme (forces light or dark colors on the ad only), and chrome (EloAdChrome.bare removes the background, border, and rounded corners; the card also drops its inner padding except a 4-point inset for the disclosure badge). EloAdLoadingView follows all three. See Styling.
  • Change: the CTA label stays readable on a custom fill. When you set callToActionBackground and leave callToActionForeground nil, the label is black or white, whichever contrasts more with your color. It used to be white.
  • Fix No empty gap before the headline on iOS 26 when an ad has no image. The card and the strip left 12 and 10 points where the image would be; the text now starts at the edge padding, as on earlier iOS versions.
Breaking changes GitHub release
  • Breaking The SDK reports display_position, you no longer pass it. The parameter was a hint about a surface the SDK already knows: it renders the ad, so it knows whether the ad landed in the chat-row card or the keyboard strip. It now reports card for .compactHorizontal and banner for .inlineBanner from the view itself, and a request made through Elo.loadAd / Elo.preloadAd directly carries no position at all. Two consequences worth knowing: the position is out of the preload cache key, so a preloaded ad is served to whichever view later loads the same transcript instead of being stranded when the positions disagree, and DiagnosticsEntry.displayPosition is now the string the SDK reported rather than the one you passed. Reporting is unaffected: the values reaching your dashboards are the same ones the slots were already sending. Remove the argument and nothing else changes.
  • New Taps inside an Elo-served ad are reported to Elo analytics. EloAdView now sends an ad_tap event (endpoint sdk/events) for every tap inside an Elo-served card or strip: the region under the finger (thumbnail, headline, description, CTA, chevron, or chrome), the tap position as a fraction of the ad view’s own size, the view’s size, whether the tap opened the destination, the time since the impression, and a per-showing tap index. Taps on the inert parts of a strip that carries a CTA are reported as not opened, which is what makes the missed-CTA rate and the tap heatmap measurable. Coordinates are relative to the ad view and never to the screen, at most fifty taps are reported per showing, and mediated fills are excluded. Nothing about how taps behave changed, and the event is analytics only: it is never billed and it does not affect click reporting. Diagnostics follow it as a .tap tracking event, so TrackingDiagnosticsEntry.Event has a new case; a switch over that enum without a default needs the extra case. Declare it as Product Interaction in your nutrition label if you have not already; see Privacy & Consent.
  • New Server-selected call-to-action treatments on the keyboard strip. An Elo-direct creative can now ask for one of three treatments beyond the existing control: a decorative arrow.up.right after the label, a single pop of the whole button, or one shimmer sweeping across the CTA pill. The ad server picks it per creative; there is no publisher-facing setting, and a missing or unknown value renders the control CTA you see today. The arrow widens the pill it is added to; the pop and the shimmer are drawn and leave the layout and the tap target alone. Motion starts only after one continuous second at 50% visibility, repeats every few seconds while the strip stays on screen, and rests at the static appearance in between; it cancels off screen and is suppressed entirely by Reduce Motion. Compact cards and mediated renderer-backed ads are unchanged.
  • The strip’s creative mark is a rounded square with no border. It keeps its 40pt size and its disclosure badge; the corner radius is now 10pt and the hairline ring introduced in 0.4.1 is gone. Ad servers pick a favicon or an apple-touch-icon for the creative image, and those are drawn for the app-icon silhouette, so a circular crop was cutting the ends off the artwork: measured across 90 days of served creatives, the disc removed 9.7% of the average mark and the new radius removes 2.9%. Both layouts now use a rounded square. Nothing in the layout moves.
GitHub release
  • Fix An ad faded in by the host app now reports an impression. Viewability treated a transparent ad as not on screen, and nothing recomputes it when opacity changes, since the measurement only wakes on layout, geometry, and window changes. An app that animated its ad slot up from opacity(0) therefore latched the ad at “nothing visible” for its whole showing: fully opaque and in front of the user, but never able to reach the 50%-for-one-second bar, so no impression fired. Renders were unaffected, which is why the slot looked healthy while impressions fell away. Visibility is now measured geometrically, from what clips the ad and from the window, never from alpha or isHidden, matching how the Android SDK has always measured. Publishers who fade or cross-fade an ad slot should expect their iOS impressions to return to the render volume they already see. Introduced in 0.3.0.
  • Fix An ad covered by an overlay, or on screen while the app is not frontmost, no longer counts as an impression. A sheet, alert, or any view drawn over the ad left its frame untouched underneath, so a fully covered ad measured as fully visible; backgrounding was invisible the same way. Neither fires a layout or geometry event, so an in-flight dwell was never cancelled either. The geometric fraction is now scaled by the share of it the window’s hit test still reaches, the dwell requires the app to be frontmost, and a bounded periodic probe restarts the full one-second dwell whenever a cover appears. Removing the cover starts a fresh dwell; leaving the foreground cancels an armed one, and returning arms a new one. What this catches is a cover with a view of its own above your screen. Three kinds stay undetectable, as they are for any hit-test based check: a cover that declines touches itself (allowsHitTesting(false)), a SwiftUI sibling drawn over the ad inside the same view (a ZStack or .overlay scrim has no view of its own for the hit test to return), and a cover in a separate UIWindow.
  • Fix A retained ad view no longer re-reports a render or impression after a reconfigure. The one-render-one-impression-per-opportunity latch now lasts the life of the process, across both Elo.configure() and Elo.shutdown(), so remounting a view held from before cannot bill the same opportunity a second time. Until this release, shutdown() cleared that state and an ad shown before it became reportable again after the next configure.
  • The disclosure badge moved onto the creative mark, and was restyled. 0.4.0 badged it into the surface’s top-trailing corner; it now rides the top-trailing corner of the creative mark itself, hung off that corner rather than sitting flush inside it, so it nests against the artwork instead of covering it. Badging the artwork reads as marking the creative rather than the container, and it keeps the disclosure clear of whatever closes the row: the surface corner overlaid the call-to-action pill whenever one ran to the edge. The badge itself is a near-white disc with a hairline black ring and black copy at medium weight, squared up from a capsule since the default copy is two characters; longer copy relaxes it back into a capsule rather than being squeezed. Those are the only fixed colors in the SDK, because the badge sits on creative artwork rather than on a surface the theme controls: a fill that followed light and dark would read against one creative and vanish into the next. EloAdDisclosure.color and EloAdStyle.badgeColor still override it, which takes the ring off and inverts the copy as before. A creative with no artwork has no mark to ride, so there the badge keeps its 0.4.0 position in the surface’s top-trailing corner. The copy, the sponsoredLabel API, and the guarantee that the disclosure is always drawn and never truncated are unchanged.
  • The strip’s creative mark is larger and ringed, without the strip changing height. The strip’s row is pinned to the call-to-action button’s 44pt tap target, so its mark grew into height the row already paid for (34pt to 40pt), and it now carries a hairline grey ring, so a white or near-white logo has an edge of its own instead of dissolving into the surface at that size. The card’s mark is unchanged at 56pt and is large enough to stand without a ring: the card’s row is pinned to the mark itself, so a larger one would have come out of the card’s own padding and crowded the text column. The card stays 80pt tall, horizontal padding is untouched on both layouts, and EloAdLoadingView’s skeleton follows, since a placeholder that lands at a different height than the fill shoves the surrounding UI.
  • Fix An ad request can no longer stall on WebKit. The SDK reads the browser user agent once per install by spinning up a WKWebView, and that read sits on the path every bid awaits. WebKit’s cold start is unbounded (it has to launch its own WebContent and GPU helper processes first, measured at over five minutes each on a cold simulator) and the auction deadline could not cancel it, because the underlying callback is not cancellable. On an unlucky first launch that meant an ad request hanging far past the point the auction had given up. The fetch is now bounded: if the browser user agent is not available within two seconds the request proceeds without it, and a later request picks it up once WebKit is warm. The value is still cached after the first successful read, so steady-state behavior is unchanged.
  • Fix EloChatSession.setMessages no longer fires a second, unusable ad request per user turn. It preloaded with no display position while every slot loads with one (.banner for the keyboard banner), and the display position is part of the preload cache key, so the preloaded ad could never be taken and the slot always went to the network anyway. Each user turn therefore billed two ad opportunities for one render, halving every opportunity-based ratio on your dashboards; verified end to end against a live ad server at 29 opportunities for 14 renders before, 7 for 7 after. setMessages now only stores the transcript. To warm the cache, call Elo.preloadAd(...) yourself with the position the slot will render into.
Breaking changes GitHub release
  • The ad disclosure moved off the headline into a corner badge. It used to lead the attribution line as Ad • Headline, costing the copy roughly five characters of the narrowest column in the SDK — on the keyboard strip especially, space the headline needs. It is now a small capsule overlaid in the surface’s top-trailing corner, so the headline gets the full width of its column and the disclosure still cannot be truncated: it is outside the text flow entirely rather than merely at the head of it. The badge applies to both layouts, and card height is unchanged. Two consequences: EloAdStyle.badgeColor now fills the capsule (the copy is drawn over it in the card background color), and a UI test asserting the combined "Ad • Headline" label must be split, since the disclosure and the headline are separate elements now.
  • Breaking sponsoredLabel takes an EloAdDisclosure. The new value carries the disclosure copy plus an optional font, color, and accessibilityIdentifier, so you can restyle the badge and give UI tests a stable handle on it. It is a value rather than a view on purpose: EloAdView draws the badge itself, overlaid on the surface, which is what makes it untruncatable — a publisher-supplied view would have to be laid out beside the headline, where a long headline can squeeze or clip it. EloAdRenderConfiguration.sponsoredLabel is unchanged and still a String: adapters build their own native views, so there is nothing there for a font or an identifier to bind to. Mediated fills keep the look they have today, taking your disclosure copy and rendering it in the adapter’s own treatment. See EloAdDisclosure.
  • Breaking callToActionLabel is gone. Call-to-action copy is backend-managed now: it arrives on the creative as EloAd.ctaText and labels the pill both layouts draw. The publisher-supplied parameter only ever reached renderer-backed fills, and no bundled adapter read it, so nothing that shipped ever rendered it. Drop the argument from your call sites; EloAdRenderConfiguration.callToActionLabel goes with it.
  • Breaking openLinkAccessibilityLabel is gone, along with the VoiceOver hint it carried and the matching EloAdRenderConfiguration field. See the accessibility callout above.
  • New EloAd.ctaText: some demand sources send a call-to-action label (Install, Learn more) alongside the creative. Where one arrives, both layouts draw it as a pill in the trailing slot in place of the disclosure chevron, colored by EloAdStyle.callToActionBackground / callToActionForeground. See EloAd.
  • The two layouts take deliberately different tap rules. The in-chat card stays one tap target from edge to edge whether or not the creative sends a label: in a transcript the card reads as a single object and there is no neighbouring control for a stray tap to hit, so its pill is decoration rather than a second button. The keyboard banner does the opposite — when the creative sends a label, only that button taps, because the strip sits under the reader’s thumb beside the composer. A strip without a label keeps its whole row tappable behind the chevron. Either way the tap opens the same destination, and render and impression tracking stay on the container, so viewability is unaffected.
  • An image-only creative no longer gets an invented description. A creative with an image but no description used to render the SDK’s own copy, Click to learn more →, whenever there was no CTA button. With the call-to-action backend-managed, the SDK writes no call-to-action of its own: such a creative now renders its headline and nothing beneath it, with the trailing chevron still signalling the row is tappable. Android never had this fallback, so both platforms now agree.
  • The keyboard banner shows its loading skeleton by default. showLoadingPlaceholder on .eloKeyboardBannerAd now defaults to true, matching EloAdView. The skeleton stands at the strip’s own size, so the composer above keeps its place when an ad arrives and the slot reads as loading rather than as empty. Pass false to keep the keyboard edge clear until an ad fills.
  • Fix A slow creative image no longer holds the whole ad in its skeleton. The thumbnail now carries its own shimmer and swaps in when the image finishes, so the headline and description render as soon as the ad does.
  • The keyboard banner’s strip was retuned to fit real creatives: a circular brand mark, tighter gutters, and a smaller type scale on the attribution line. At the previous size an ordinary advertiser name ellipsized before the strip had drawn anything else.
  • New Elo.setUserData(_:): apps that know their signed-in user can share age, gender, email, and phone on ad requests. The fields are recorded for upcoming targeting features and have no effect on ad selection today. Pass contact details as they are — they travel over HTTPS and are SHA-256 hashed at the ad server before anything is stored, so Elo never persists a plain email address or phone number and you don’t hash them yourself. Give a phone number its country code (E.164) or the server discards it. The SDK does not validate what you set: the server normalizes each field and drops what it can’t use. Each call replaces the whole object. The ad server discards user data entirely on requests flagged COPPA or TFUA, and on any request where GDPR applies. Sharing contact details is a disclosure obligation: see the API Reference and Privacy & Consent.
  • Publisher-supplied identity now survives an app restart. The identifier from setUserIdentifier and the data from setUserData are stored as one encrypted record in the Keychain, as a ThisDeviceOnly item so it never reaches iCloud Keychain or an encrypted device backup, and read back at launch. An app that keeps people signed in between launches used to report as anonymous on every relaunch until the user happened to sign in again; it no longer does. Set both from a single auth-state hook: that is the call site that covers sign-in, sign-out, and a mid-session account switch, and the one that keeps a stored copy from going stale when the user changes their email or phone. shutdown() erases the stored record. Nothing is ever written in plaintext — if the Keychain is unavailable the SDK keeps identity in memory for that process rather than falling back to an unprotected file. Storing contact details on the device is worth a line in your privacy policy and nutrition label.
Breaking changes GitHub release
  • Fix An ad hidden behind a container no longer counts as viewable. Impressions require the ad to be at least half on screen for one continuous second, but “on screen” was measured against the whole window, against a frame that ignored clipping. An ad scrolled out of a transcript, inset by a container, or covered when the transcript shrinks for the keyboard therefore measured as fully visible and could fire an impression with none of it in front of the user. Viewability is now measured against what actually clips the ad, matching how the Android SDK has always measured.
  • Fix The second and later ads shown in the same slot now report an impression. The dwell measurement was reset to “nothing visible” whenever a new ad replaced the one already displayed, and nothing recomputed it, because the view had not moved. Every ad after the first in a slot that stayed mounted (the usual case when a chat requests a fresh ad without the view going away) recorded its render and its click but never its impression. The dwell is now keyed on the ad opportunity, so a swap restarts the measurement on its own.
  • Fix Calling Elo.trackRender(_:) or Elo.trackImpression(_:) yourself no longer double-counts. The one-render-one-impression-per-opportunity limit used to live in the view layer, so a fully custom layout calling the tracking hooks directly could report the same opportunity more than once. It now sits behind the public API and covers every path into it, so the hooks are safe to call on every layout pass. A repeat load that returns the same creative is a distinct opportunity and still records. A suppressed duplicate is silent: no ping, and no eloAdDidTrackImpression callback.
  • New DiagnosticsSnapshot.trackingTotals: a per-event count of every render, impression, and click attempt since configure. trackingEntries is a bounded ring buffer, so it could not tell you how many pings failed once a session got past the newest twenty, and a ping the SDK gives up on is a lost impression it never retries. Each TrackingTotals carries attempted, delivered, failed, unobservable, and inFlight, is never evicted, and appears in asExportableText(). Cleared on configure and shutdown(), like the rest of diagnostics. See Diagnostics.
  • Fix Re-configuring the SDK now clears tracking state, matching Android. Elo.configure already replaced the session, the ad cache, and the network client, but left the render/impression bookkeeping behind, where it could suppress tracking for an ad shown under the new configuration.
  • Fix An ad card no longer loses its image when the same creative comes back after you leave and re-enter a chat. The card hides its thumbnail when a creative’s image cannot be loaded, so a broken URL leaves text rather than a blank square. That suppression fired on cancelled image fetches too, and iOS cancels the fetch in flight whenever the view goes away, which is what navigating out of a chat does. It was also remembered against the creative rather than the ad, so one failure suppressed the image on every later ad serving that creative. A cancelled fetch is now left alone and retried on the next showing, and the suppression applies only to the ad it was recorded for.
  • Adapter authors EloAd.init now takes both id and creativeId, and id must be unique per fill. Pass your network’s per-response id if it has one, or mint a UUID().uuidString. Passing a creative id there collapses every showing of that creative into a single tracked ad, so only the first reports a render and an impression.
GitHub release
  • New Elo.setUserIdentifier(_:): apps with a sign-in can send their own user ID as the visitor_id on ad requests and tracking pings in place of the SDK’s anonymous per-install ID, so delivery, frequency capping, and reporting follow the user across installs and devices. Memory-only: re-set it on each launch and pass nil on sign-out to restore the anonymous ID. shutdown() clears it; a re-configure does not. An ad keeps whichever identity it was requested under for its whole lifetime. See the API Reference.
  • A scrolling ad description now makes three passes and then settles into a tail-truncated line instead of animating for as long as the ad is on screen. The endless marquee kept the host app from ever going idle (timing out XCUITest and Appium runs) and leaked memory on some OS releases; the scroll is now stepped on a fixed 30 Hz clock with visually unchanged motion, and always starts flush from the head of the line even when a new creative interrupts a pass mid-scroll.
  • The loading placeholder’s shimmer sweeps a few times and then rests as a plain skeleton, and stays still under Reduce Motion, instead of repeating for as long as the placeholder is on screen.
  • A creative that wins more than once now records a render and an impression every time it’s shown, with re-appearances of the same opportunity (lazy-list re-entry, tab switches) still reporting exactly once. Two API-visible consequences: EloAd equality and hashing now distinguish the same creative served for two different ad requests, and Elo.shutdown() clears the dedup state, so an ad shown before shutdown can report again after a re-configure.
GitHub release
  • The ad disclosure now leads the attribution line: Ad • Headline (previously Headline · Sponsored). Leading the disclosure makes it structurally impossible for a long headline to truncate it away; the separator is now • and sponsoredLabel’s default changed from "Sponsored" to "Ad". VoiceOver announces the disclosure first. The parameter name is unchanged — keep passing a localized string for non-English surfaces.
  • Creative descriptions are now a single line that marquee-scrolls when the copy doesn’t fit, so the whole line stays readable instead of wrapping. Motion pauses while the ad is off screen and falls back to a static ellipsis under Reduce Motion. Opt out with the new EloAdStyle.descriptionOverflow: .truncate. Compact cards with long descriptions are roughly 19pt shorter now that the text no longer wraps to two lines.
  • An ad image that fails to load now hides the thumbnail instead of leaving a blank tile; the text takes the reclaimed space. AdMob-rendered fills apply the same rule to a missing icon asset.
Breaking changes GitHub release
  • Fixed impressions never firing for SDK-rendered creatives on recent OS releases. The viewability plumbing moved to onGeometryChange, restoring the ≥50%-visible-for-1-second impression contract.
  • Privacy the X-Elo-State session header is only attached to requests on the configured API origin (and stripped if a redirect leaves it), and the Device-Name / System-Version HTTP headers are removed — device/OS context already travels in the User-Agent and the OpenRTB device object.
  • Preloaded ads are keyed on display position and the consent snapshot in addition to messages and context, so a preload is never served for a different slot position or after a consent change.
  • New MessageRole.summary for hosts that condense long transcripts into one summary message plus the latest exchange, and new NoFillReason.other(String) for uncategorized reasons (both Android parity).
  • AdNetworkAdapter gains an optional shutdown() teardown hook (default no-op), invoked from Elo.shutdown() and on re-configure.
  • The mediation auction deadline is now the tighter of the per-request timeout and the configured auction timeout, so adapters and the mediator agree on the deadline.
  • Impression and click delegate callbacks are no longer delivered after Elo.shutdown() or a re-configure.
  • Elo render and impression trackers now require 2xx responses instead of silently swallowing delivery failures; their delegate callbacks wait for confirmed Elo delivery. Elo-direct click POST delivery is temporarily disabled — tapping still opens the creative destination and delivers publisher callbacks.
  • Diagnostics: Elo.Debug.snapshot() adds trackingEntries (a bounded, privacy-safe history of tracking attempts), includes preload outcomes in entries, and stamps load entries with a locally generated operationId.
  • Ad request payloads omit the context key entirely when no context objects are provided (Android wire parity).
Breaking changes GitHub release
  • Elo.initialize is deprecated and will be removed in 1.0. Elo.configure is now the single entry point, in two forms:

    • Elo.configure(publisherId:adUnitId:shareGeoLocation:geoLocationPrecision:) — new convenience for Elo-only integrations; the geo controls keep the on-by-default sharing opt-out visible at the simplest entry point.
    • Elo.configure(with: EloConfiguration) — unchanged; mediation adapters, COPPA/TFUA, logLevel, and baseUrl live here.

    Migrating from initialize is a rename for most apps; if you passed coppa: / tfua:, move them onto EloConfiguration.

  • EloError.notConfigured’s description now points at Elo.configure(with:).

  • The example app is now generated from the SDK sources and compile-checked against the SDK on every release, so it can no longer drift from the published API.

GitHub release
  • New keyboard banner format: .eloKeyboardBannerAd(messages:) pins an .inlineBanner strip above the keyboard with one modifier — see Keyboard banner.
  • Elo.initialize(...) gains shareGeoLocation / geoLocationPrecision parameters.
  • Ad requests now carry a settings-derived country (device.geo.country) and UTC offset (device.geo.utcoffset) — permission-free, from locale/time-zone settings, not GPS.
  • Elo-rendered creatives drop the CTA pill; the whole card is tappable. callToActionLabel now only affects renderer-backed mediation fills.
  • Fixed: renderer-backed ads size to their intrinsic height again; preloaded creatives are released on shutdown/reconfigure (no more leaked AdMob NativeAd handles); collapsed banner slots reload when messages changes.
  • The host-app privacy guide (PRIVACY.md) now ships in the public package.
GitHub release
  • Repaired the public SwiftPM release path after 0.1.3/0.1.4 binary assets drifted from their recorded checksums; release assets are now immutable and verified after publishing.
GitHub release
  • Complete OpenRTB device object on every request (user agent, model, OS, screen, language, connection type).
  • Privacy-gated advertising identity: ifa only with ATT authorization; lmt derived from ATT status.
  • Opt-in passive geo sharing introduced (shareGeoLocation, off by default at the time — see 0.1.6).
GitHub release
  • EloAdsMediationAdMob ships as a binary XCFramework alongside EloAds, with Google Mobile Ads dependencies delivered through SwiftPM.

0.1.2 added view-level layouts (EloAdLayout, .eloAdLayout(...)) and standardized impression tracking (≥50% visible for 1 second). 0.1.0 renamed the SDK from Growl to Elo (GrowlAds → EloAds, breaking). See GitHub Releases for the full history.