iOS guide

Show a TaleArt zone in an iOS app with the iOS SDK: a view controller for UIKit, or a SwiftUI view made from it. You need the zone key from the dashboard, the same key the Web SDK uses. The iOS SDK sends analytics events too (see Analytics below), so the zone's Analytics tab counts the app along with the web.

Before you start

  • iOS 15 or later, Xcode and Swift. The framework is built for devices (arm64) and for the simulator on Apple silicon.
  • A zone with a display, and its key (tak_…), from the zone's Install tab in the dashboard.

Steps

  1. Add the SDK

    The easiest way is Swift Package Manager: in Xcode choose File > Add Package Dependencies and enter the repository URL. The repository's Package.swift points at the TaleartSDK.xcframework.zip attached to a GitHub release, so the version you choose is the framework you get.

    // Xcode: File > Add Package Dependencies, then enter the repository URL.
    // Or in a Package.swift:
    dependencies: [
        .package(url: "https://github.com/ido-lempert/Taleart", from: "0.1.1"),
    ],
    targets: [
        .target(name: "MyApp", dependencies: [
            .product(name: "TaleartSDK", package: "Taleart"),
        ]),
    ],

    Or add the framework by hand: download TaleartSDK.xcframework.zip from the release page, unzip it, and drag TaleartSDK.xcframework into your app target. Set its embed option to match the framework's type: embed and sign a dynamic framework, do not embed a static one (its Info.plist says which).

    Diagram: your Xcode project adds the Taleart package, which downloads TaleartSDK.xcframework.zip from a GitHub release. Dragging the unzipped framework into the target is the other way.
    Diagram of the two ways to add the SDK (not a screenshot).
  2. Get the zone key

    On the zone's Install tab in the dashboard, copy the key (tak_…). The key is shown once, when it is issued. Put it in your code as a value, never in a URL. It reads that one zone, so treat what the zone shows as public. Rotating the key issues a new one and stops the old one at once.

    Diagram of the dashboard's Install tab for a zone: the zone key starts with tak_, is shown once, and has a Copy button.
    Diagram of the Install tab (not a screenshot).
  3. Place the zone: UIKit or SwiftUI

    TaleartZoneView shows one zone. Its viewController is the controller to embed, and onHeightChange reports the bar's or banner's height, so the container can size to it. Keep the TaleartZoneView alive as long as the controller: it holds the zone's state. A UIKit screen embeds it as a child controller.

    import UIKit
    import TaleartSDK
    
    final class HomeViewController: UIViewController {
        private let zone = TaleartZoneView(apiUrl: nil, zoneKey: "tak_YOUR_ZONE_KEY")
        private var heightConstraint: NSLayoutConstraint?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            let bar = zone.viewController
            addChild(bar)
            bar.view.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(bar.view)
            let height = bar.view.heightAnchor.constraint(equalToConstant: 0)
            heightConstraint = height
            NSLayoutConstraint.activate([
                bar.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                bar.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                bar.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                height,
            ])
            zone.onHeightChange = { value in height.constant = CGFloat(value) }
            bar.didMove(toParent: self)
        }
    }

    In SwiftUI, wrap the controller in a UIViewControllerRepresentable. Its coordinator keeps the TaleartZoneView, and the height goes back to a state value:

    import SwiftUI
    import TaleartSDK
    
    struct ZoneBar: UIViewControllerRepresentable {
        let zoneKey: String
        @Binding var height: CGFloat
    
        // The zone object must live as long as the view controller: the coordinator keeps it.
        func makeCoordinator() -> Coordinator { Coordinator(zoneKey: zoneKey) }
    
        func makeUIViewController(context: Context) -> UIViewController {
            let zone = context.coordinator.zone
            let height = $height
            zone.onHeightChange = { value in DispatchQueue.main.async { height.wrappedValue = CGFloat(value) } }
            return zone.viewController
        }
    
        func updateUIViewController(_ controller: UIViewController, context: Context) {}
    
        final class Coordinator {
            let zone: TaleartZoneView
            init(zoneKey: String) { zone = TaleartZoneView(apiUrl: nil, zoneKey: zoneKey) }
        }
    }
    
    struct HomeView: View {
        @State private var barHeight: CGFloat = 0
    
        var body: some View {
            VStack {
                ZoneBar(zoneKey: "tak_YOUR_ZONE_KEY", height: $barHeight)
                    .frame(height: barHeight)
                Spacer()
            }
        }
    }
    Screenshot of the TaleArt sample app on iOS showing only the zone: a bar of round covers in gradient rings with the group names Summer, New arrivals, Winter and Trips under them, on a plain light page.
    The bar in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator. It shows only the zone (its taller.zoneOnly debug option), so the picture holds nothing but the SDK's own output. The zone has six groups, and the row scrolls sideways.
  4. Initialise once at launch

    Set the SDK's configuration once, at launch, before any zone view shows. Every view reads it; a later call replaces it, and the views read it again. Set apiUrl: a native app has no page to take the origin from, and with an empty one a zone shows nothing. The call is doInit(config:) in Swift, because Kotlin's init is named that way for Objective-C.

    import SwiftUI
    import TaleartSDK
    
    @main
    struct MyApp: App {
        init() {
            TaleartSdk.shared.doInit(config: TaleartConfig(
                apiUrl: "https://api.example.com",
                locale: "he-IL",
                strings: ["he": ["viewer.close": "סגור", "bar.seen": "{name}, נצפה"]],
                stringsForEveryLocale: [:],
                listener: AppTaleartHooks(),
                externalUserId: nil,
                tags: [],
                properties: nil,
                fallback: FallbackMode.none,
                frequency: nil,
                analytics: true,
                layoutDirection: nil
            ))
        }
    
        var body: some Scene {
            WindowGroup { HomeView() }
        }
    }
    
    // UIKit: the same call in application(_:didFinishLaunchingWithOptions:) of your AppDelegate.
    Diagram: the app calls TaleartSdk once at launch. The configuration holds apiUrl, locale, strings and listener, and every zone view reads it.
    Diagram of the start-up order (not a screenshot).
  5. Pass viewer properties

    Pass what the app knows about the viewer as properties: a dictionary from a name to a String, a number, a Boolean or an array of Strings. A tale group can carry an audience rule, set in the dashboard under its targeting, and the SDK shows the group only when the rule passes these values. The rule can test a value with eq, in, gt, contains, exists and the other operators listed on the Targeting page.

    Put the properties in TaleartConfig at launch. Swift needs every argument of the configuration, so pass nil when the app has none:

    // The properties argument of TaleartConfig(...) in doInit. Swift needs every argument,
    // so pass nil when the app has none.
    properties: [
        "plan": "pro",                      // a String
        "age": 34,                          // a number
        "newsletter": true,                 // a Boolean
        "segment": ["beta", "newsletter"],  // a list of Strings
    ],
    
    // One zone only: its properties replace the SDK's, and the zone is read again at once.
    let zone = TaleartZoneView(apiUrl: "https://api.example.com", zoneKey: "tak_YOUR_ZONE_KEY")
    zone.properties = ["plan": "free"]
    // Back to the SDK's properties: zone.properties = nil

    A name starts with a letter and uses letters, digits, _, . or -, up to 64 characters, and is case-sensitive. A dictionary has at most 50 properties, a string at most 256 characters and an array at most 50 items. An invalid entry is ignored. Dates go as epoch milliseconds.

    With no properties (nil) every group's audience rule passes, as it does with no tags. An empty dictionary means the app knows nothing about the viewer: a group whose rule needs a value is then left out, while the negative conditions (neq, notIn, notExists) pass.

    A group shows only when its tags and its audience rule both pass. The properties stay on the device: the SDK sends them in no request header, no URL and no analytics event. The rule itself is public, as the tag names are, so keep private content out of rule values.

    A zone can use its own properties, which replace the SDK's for that zone only. Setting properties on a TaleartZoneView reads its zone again at once. A later doInit applies its properties on the next read of a zone: a zone already on screen keeps the groups it shows until it reads again, as it does for tags.

  6. Listen to hooks and override a link

    Subclass TaleartListenerBase in Swift and override the hooks you need. Its methods do nothing by default, so override only what you use. Pass the subclass as listener in the configuration, or on one zone view. The view's listener runs first, then the global one.

    onActionClicked returns a Boolean. Return true when your app routed the link, and the SDK does not open it. Return false and the system opens it in the browser.

    import TaleartSDK
    
    final class AppTaleartHooks: TaleartListenerBase {
        override func onLoaded(event: TaleartEvent) {
            print("zone \(event.zoneSlug) loaded")
        }
    
        override func onActionClicked(event: TaleartEvent) -> Bool {
            guard let url = event.url, url.hasPrefix("https://shop.example.com/") else { return false }
            AppRouter.shared.open(url)   // your own navigation
            return true                  // handled: the SDK does not open the browser
        }
    }

    The methods, their payloads and the events are in the iOS SDK reference. onGroupImpression fires on native too, once per group per load. onComponentEvent reports a countdown's end and a promo code's copy, and onUserInteracted reports each answer the server accepts to a poll, a quiz, a rating or a reaction.

    Diagram: a zone event goes to the view's listener, then to the global listener. If onActionClicked returns true the app routes the link; if it returns false the system opens the browser.
    Diagram of the hook order and the link decision (not a screenshot).
  7. Localise the texts

    The SDK's texts (the viewer's buttons, the banner's controls, the names it announces) are English by default. Set locale (BCP 47, such as he-IL) and the texts per locale. A text is looked up under the full locale, then its language, then stringsForEveryLocale, then English. A missing key shows English, never the key. With no locale set, the device's locale is used. A single zone view takes its own locale and strings:

    // At launch (the config above), or for one zone:
    let zone = TaleartZoneView(apiUrl: "https://api.example.com", zoneKey: "tak_YOUR_ZONE_KEY")
    zone.locale = "he-IL"
    zone.strings = ["he": ["viewer.close": "סגור"]]

    Each string key and its English default is in the iOS SDK reference.

    Diagram: a configuration with locale he-IL and Hebrew strings, and a phone whose viewer shows the close label in Hebrew and a seen label reading name then, in Hebrew.
    Diagram of the localised result (not a screenshot).
    Screenshot of the TaleArt sample app's viewer on iOS with the Hebrew locale and right-to-left layout, open on a Hebrew group: a pale sun over a sea gradient, the group name in a chip on the right, the close and pause buttons on the left, and two progress bars.
    A Hebrew group in the sample app's viewer (taller), a real screenshot taken in the simulator. The group name and the controls sit by right-to-left rules. The viewer's buttons are icons in this shot, so it does not show their localised labels.
  8. Verify

    Run the app in the simulator (Apple silicon) or on a device, then check, in order:

    • The bar (or banner) appears where you placed the zone view, and takes its height.
    • A tap on a circle opens the viewer, and Close (in your locale) dismisses it.
    • The Xcode console shows the SDK's messages, marked TaleArt. A wrong key shows no zone and logs one failure; onLoadFailed gets the message.
    Diagram: a phone with the viewer open, and a checklist: the bar shows the zone, a tap opens the viewer, Close returns to the bar, and onLoaded is printed to the console.
    Diagram of the checks (not a screenshot).
    Screenshot of the TaleArt sample app's viewer on iOS, open on the group Summer: a pale sun over a gradient of sand and sea, the group name in a dark chip at the top left, the pause and close buttons at the top right and two progress bars along the top. Dark bands fill the screen above and below the tale.
    A tale open in the viewer, a real screenshot taken in the simulator. The viewer is running (the Pause button shows, not Play), and the first of the group's two progress bars is filling. The tale is 9:16 and this phone is taller, so the dark ground shows above and below it.
    Screenshot of the TaleArt sample app's debug settings screen on iOS, not an SDK screen: an API URL field, a zone key field that shows only its first eight characters, tak_demo, followed by dots, and Load and Cancel buttons.
    The settings screen of the TaleArt sample app (taller), not an SDK screen. It is where the sample app takes its API URL and zone key. The key is masked after its first eight characters in this picture.

Show a message when a zone fails

By default a zone that cannot be read shows nothing. Set fallback: FallbackMode.message in TaleartConfig to show the translated "Stories could not be loaded." text with a "Try again" button, which reads the zone again. A zone can show your own view controller instead with fallbackViewController, at fallbackHeight points (120 by default); it wins over the message.

let zone = TaleartZoneView(apiUrl: "https://api.example.com", zoneKey: "tak_YOUR_ZONE_KEY")
zone.fallbackViewController = myPlaceholder   // shown instead of the zone when the read fails
zone.fallbackHeight = 80                      // its height in points (default 120)
Screenshot of the TaleArt sample app on iOS when its zone cannot be read: on a plain page, the centred message 'Stories could not be loaded.' with a Try again button under it.
The fallback message in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator. The sample app asks for a key the stand-in API refuses, and shows only the zone (its taller.zoneOnly debug option), so the message and the button are all there is.

Cap how often a viewer sees a group

frequency hides a tale group after the viewer has seen enough of it. A view is one opening of the viewer on the group. The count stays on this device and is never sent to TaleArt. Groups over the cap leave the display, and when none are left nothing shows. The default is no cap: pass nil.

TaleartSdk.shared.doInit(config: TaleartConfig(
    apiUrl: "https://api.example.com",
    locale: "",
    strings: [:],
    stringsForEveryLocale: [:],
    listener: nil,
    externalUserId: nil,
    tags: [],
    properties: nil,
    fallback: FallbackMode.none,
    frequency: FrequencyRuleOnce.shared,    // or FrequencyRuleDaily.shared, FrequencyRuleWeekly.shared,
                                            // or FrequencyRuleCount(views: 3) for 1 to 50 views
    analytics: true,
    layoutDirection: nil                    // or "rtl" / "ltr" to set the direction; nil follows the device
))
Screenshot of the TaleArt sample app on iOS with a frequency cap of once, before any view: the zone bar shows three round covers in gradient rings, labelled Morning, Afternoon and Evening.
The zone bar before the viewer has opened a group, with the cap set to once (taller.frequency in the sample app), a real screenshot taken in the simulator.
Screenshot of the same TaleArt sample app on iOS after the group Morning was viewed once and the app opened again: the zone bar shows only two covers, Afternoon and Evening, and the first group is gone.
The same zone after the viewer had opened the first group, Morning, once: the group has left the bar and the other two stay (a real screenshot taken in the simulator).

Bar layouts

A bar zone draws its groups in one of four layouts, set by the bar's layout setting in the dashboard. Ring is the default. The other bar settings (shape, size, titles, alignment and gap) apply where the table says so, and the rest are ignored. While the zone is read, the bar shows a loading skeleton in the footprint it had last, so the page does not move when the groups arrive. The skeleton is announced as busy, and it stands still under reduced motion.

The bar layouts and the settings each one ignores
LayoutUseIgnored settings
ringThe default. Each cover sits in a gradient ring with its title under it; the ring turns while a group is unseen and stays grey once it is seen.None.
chipA pill per group: a round cover, its title, and a dot while the group is unseen (the seen pill is muted and has no dot). The pills wrap onto more rows.shape. The size sets the cover (28, 32 or 40 dp).
posterA portrait card per group: the cover fills it, with the title and the tale count under a dark scrim, and a segment for each tale along the top.shape and size. The card is 112 by 188 dp and has a fixed corner radius.
stackThe groups overlap in one collapsed pill: up to four round covers and a +N bubble for the rest, with the count of unseen groups beside them. The pill opens the first unseen group.shape, showTitles, alignment and gap. The size sets the covers (40, 48 or 56 dp).
Screenshot of the TaleArt sample app on iOS with the bar in the ring layout: round covers in gradient rings with the group names Summer, New arrivals, Winter and Trips under them.
The ring layout, the default. The sample app (taller) sets the layout with its taller.barLayout debug option and shows only the zone; a real screenshot taken in the simulator. Its zone has six groups, and the row scrolls sideways.
Screenshot of the TaleArt sample app on iOS with the bar in the chip layout: six pills, each with a round cover, the group name (Summer, New arrivals, Winter, Trips, Food, Music) and a small ring for a group not yet seen, two pills to a row.
The chip layout, a real screenshot taken in the simulator. The pills wrap onto several rows.
Screenshot of the TaleArt sample app on iOS with the bar in the poster layout: tall cards, each with its group's picture, a white title and a line such as '2 stories' over a dark scrim, a bar for each tale along the top, and the third card cut off at the edge because the row scrolls sideways.
The poster layout, a real screenshot taken in the simulator. Each group's card shows a bar for each of its tales.
Screenshot of the TaleArt sample app on iOS with the bar in the stack layout: one rounded pill holding four overlapping round covers and a +2 bubble for the other groups, with the line '6 new stories' beside them.
The stack layout, a real screenshot taken in the simulator. The zone has six groups, so two of them are in the +2 bubble.
Screenshot of the TaleArt sample app on iOS while its zone is still being read: four round grey placeholders with a short grey bar under each, in the footprint of the ring layout.
The loading skeleton while the zone is read, a real screenshot taken in the simulator before the zone arrived (the stand-in API held the answer back). It uses the footprint of the ring layout on the first read.
What you see: the ring bar for a moment, then the second group opens in the viewer and its progress runs, a swipe turns the cube to the next group, and Close returns to the bar, now with the groups seen gray. A recording of the sample app (taller) in the iPhone 16 Pro simulator, set up with its taller.zoneOnly and taller.autoOpenItem debug options; the swipe and the Close are mouse gestures on the simulator. It is under 8 seconds.

A banner zone shows its groups one at a time, or in one of five other variants: the frame's variant setting in the dashboard picks it. A missing or unknown value is the carousel. Each variant ignores the settings the table names, and the rest apply. The carousel has been restyled: each slide shows the group's name and a Watch stories button on a scrim, and its previous and next buttons are 48 dp, so they are easy to touch. Right to left, the line of slides, the turn of the coverflow and the fan of the deck, and the marquee run the other way, and the buttons sit on the other edge.

The banner variants and the settings each one ignores
VariantUseIgnored settings
carouselThe default. One slide at a time, with the group’s name and a Watch stories button on a scrim, previous and next buttons, dots and autoplay.None.
coverflowThe current slide in front, its neighbours turned away at the sides, the far ones smaller and fainter. Swipe, or tap a neighbour.None.
peekThe current slide in the middle with the neighbours showing at the edges, like a pager with padding.None.
deckThe slides fanned out like a hand of cards. The front card opens its group; a tap on a card behind it brings that card forward.None.
stickyA full-width bar on the card colour, pinned to the bottom of the space its host gives it and clear of the system’s bottom bar: previous and next buttons, the current slide’s round cover and name, and a dismiss button (all 44 dp, 48 dp to touch). Dismissed, it stays hidden until the app restarts.aspectRatio, showDots and loop. It always loops.
stripA continuous marquee of the groups’ covers and names, scrolling at a fixed speed (about 18 seconds a loop) toward the start of the line. A press pauses it; under reduced motion it stops and can be scrolled sideways.aspectRatio, showDots, loop, autoplay and intervalSeconds.
Screenshot of the TaleArt sample app on iOS with a banner in the carousel variant: the group name Summer and a Watch stories button over the cover on a dark scrim, previous and next arrows at the edges, and a row of six dots below.
The carousel: the group name and a Watch stories button on a dark scrim over the shown cover, the previous and next buttons on the edges and the dots under it; a real screenshot taken in the simulator.
Screenshot of the TaleArt sample app on iOS with a banner in the coverflow variant: the front card with the name Summer and a Watch stories button, the covers of the neighbouring groups turned behind it at both sides, previous and next arrows and six dots below.
The coverflow: the shown group in front, its neighbours turned away at the sides (a real screenshot taken in the simulator).
Screenshot of the TaleArt sample app on iOS with a banner in the peek variant: the front card with the name Summer and a Watch stories button, and the edges of the neighbouring cards showing at both sides, with arrows and six dots.
Peek: the shown group with the edges of its neighbours at both sides (a real screenshot taken in the simulator).
Screenshot of the TaleArt sample app on iOS with a banner in the deck variant: the front card with the name Summer and a Watch stories button, and the other cards fanned out behind it, with arrows and six dots.
Deck: the other groups fanned out behind the shown one (a real screenshot taken in the simulator).
Screenshot of the TaleArt sample app on iOS with a banner in the sticky variant: a full-width bar on a white ground with previous and next buttons, a round cover, the group name Summer and a dismiss cross.
Sticky: a full-width bar for the shown group, with the previous and next buttons, a round cover and the name, and its dismiss button (a real screenshot taken in the simulator).
Screenshot of the TaleArt sample app on iOS with a banner in the strip variant: the group names and round covers (New arrivals, Winter, Trips) scrolling across a soft grey strip, the first cut off at the edge.
Strip: the groups scrolling as a marquee, covers and names moving toward the start of the line (a real screenshot taken in the simulator, in the middle of its motion).
What you see: the coverflow advances by itself to the second group, a tap on the next arrow shows the third and stops the slide show (the button turns to Play), and the dots follow each change. A recording of the sample app (taller) in the iPhone 16 Pro simulator, under 8 seconds; the tap is a mouse click on the simulator. It stays still under reduced motion.

Analytics events

The SDK sends impression, open, view and click events for each zone, in batches and in the background, so the app never waits on them. Turn them off for the whole app with analytics: false in the configuration, or for one zone with zone.analytics; a zone's setting wins, and nil follows the configuration. On native, an impression is a group's bar item first rendered, or a banner slide first shown: the SDK does not measure whether it is on screen.

let zone = TaleartZoneView(apiUrl: "https://api.example.com", zoneKey: "tak_YOUR_ZONE_KEY")
zone.analytics = false    // this zone sends no events; nil (the default) follows TaleartConfig.analytics

Countdowns and promo codes

A tale can hold a countdown: the days, hours, minutes and seconds left to a moment in UTC, then its ended text. A tale can also hold a promo code: the code on a ticket, with a Copy stub beside it. Add either in the dashboard, as you add any element of a tale; the SDK draws it as the web SDK does. The countdown counts on the device's clock, so a visitor with a wrong clock sees a wrong time. A promo code is visible to every visitor, so it is not a secret.

Countdown

Set the end as an instant in UTC, such as 2026-12-31T21:00:00Z. The units read Days, Hours, Min and Sec, and from the end on the ended text shows (Ended, unless you set your own). The end reaches your listener once, as onComponentEvent with event.action "ended" and the countdown as event.component.

Promo code

Set the code: 1 to 32 letters, digits, _ or -. Copy puts it on the clipboard and a toast over the card's top corner says Copied for four seconds; when the clipboard refuses, it says Could not copy. A copy reaches your listener as onComponentEvent with event.action "copied" and the code as event.component.value.

import TaleartSDK

final class ComponentTaleartHooks: TaleartListenerBase {
    // A countdown's end and a promo code's copy reach the listener as componentEvent.
    override func onComponentEvent(event: TaleartEvent) {
        switch event.action {
        // A countdown reached its end, once; component.type is "countdown".
        case "ended": print("Countdown \(event.component?.id ?? "") ended")
        // A promo code was copied; the code is the component's value.
        case "copied": print("Copied \(event.component?.value ?? "")")
        default: break
        }
    }
}
Screenshot of a TaleArt sample countdown on iOS over a sunset picture: a white card reading 'Sale ends in' with 02 Days, 05 Hours, 39 Min and a seconds figure in large digits, each in a grey tile with its unit below.
A countdown in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator. The figures flip as they tick on the device's clock.
What you see: the viewer shows the countdown and its seconds tile flips each second for about five seconds. A recording of the sample app (taller) with its taller.autoOpenItem debug option.
Screenshot of a TaleArt sample promo code on iOS over a teal picture of a gift: a white card titled 'Your welcome gift' with a ticket that holds the code SUMMER25 and a Copy stub, and the note '20% off sandals, until Sunday' under it.
A promo code in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator, before a copy.
The same promo code card on iOS just after Copy was pressed: a dark 'Copied' toast sits over the card's top corner.
The same promo code after Copy, a real screenshot taken in the simulator. The toast shows for four seconds. The code is on the clipboard; iOS shows nothing of its own when an app copies.
A close-up of the promo code card on iOS just after Copy: the card's top, the ticket with the code SUMMER25 and the Copy stub, and a dark Copied toast over the card's top corner.
The Copied toast in close-up, cropped from a real screenshot taken in the simulator. It is also the poster of the clip below.
What you see: the Copy stub is pressed and a Copied toast shows over the card's corner for about four seconds. A recording of the sample app (taller) with its taller.autoCopy debug option, cropped to the card.

Polls, quizzes, ratings and reactions

A tale can hold one poll, one quiz, one rating and one reaction, each added in the dashboard as any element of a tale. A visitor answers each once: the answer is kept on the device, so a tale opened again shows the result at once. The server counts the answers and the SDK draws the counts it returns. In a preview nothing is sent or kept.

Poll

Two options as buttons. A vote shows each option's share, with the vote selected, and the result is read to VoiceOver.

Screenshot of a TaleArt sample poll on iOS over a sunrise picture: the title 'Quick poll', the question 'Tea or coffee?' and two answer buttons, Tea and Coffee, before any vote.
A poll before the visitor votes, in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator.
The same poll on iOS after a vote for Tea: Tea is selected and shows 61 percent, Coffee shows 39 percent, each with a bar filled to its share.
After a vote each option shows its share as a filled bar and the vote is selected. The result is also said to VoiceOver, as text that is never drawn (a real screenshot taken in the simulator).

Poll views

A poll is drawn in one of five views, chosen by the poll's view. The sticker is the default, and a poll without a view, or with one the SDK does not know, is a sticker. The view changes how the poll looks, not what it records. Two options can carry a colour (options[].color, #rrggbb); only this-or-that and grid use it, and without one the theme's colours are used.

ViewUseOption colour
sticker (default)A small card over the tale, for a quick question.Not used: the theme's colours.
embedA full-width card with its own results header: the vote total once voted, else the line that results appear after a vote.Not used: the theme's colours.
this-or-thatTwo halves that fill the card. Once voted they split at the vote's share and a VS mark sits on the seam; mirrored in right to left.Each half takes its option's colour, else the theme's; the text on it is white or black, whichever reads better.
gridTwo large tiles side by side, each with its label and, once voted, its share.Each tile takes its option's colour, else the theme's; the text on it is white or black, whichever reads better.
inlineCompact rows, for a poll in the flow of a page.Not used: the theme's colours.

The embed, this-or-that, grid and inline views are shown before a vote and after one. The voted images are real votes in the sample app: a vote for Midnight in each view.

Screenshot of the embed poll view on iOS: a card with the title Poll, the question 'Which colourway next?', the line 'Results appear after you vote' and two option rows, Midnight and Coral, before a vote.
The embed view before a vote, in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator.
The embed view on iOS after a vote for Midnight: the header reads '101 votes', the rows show Midnight at 58 percent, selected, and Coral at 42 percent, each with a bar filled to its share.
The embed view after a vote: its header counts the votes and each row shows its share (a real screenshot taken in the simulator).
Screenshot of the this-or-that poll view on iOS: the question 'Which colourway next?' over two equal halves, a blue half for Midnight and a coral half for Coral, with a VS mark on the seam, before a vote.
The this-or-that view before a vote: two equal halves, each in its option's colour (a real screenshot taken in the simulator).
The this-or-that view on iOS after a vote for Midnight: the blue half takes 60 percent of the card and the coral half 40 percent, the vote's label is underlined and the VS mark sits on the seam.
The this-or-that view after a vote: the seam moves to the vote's share, and each figure steps down in size when its half is narrow, so the 40 percent is never cut at the edge (a real screenshot taken in the simulator).
Screenshot of the grid poll view on iOS: two large tiles side by side under the question, Midnight in blue and Coral in coral, before a vote.
The grid view before a vote: two large tiles (a real screenshot taken in the simulator).
The grid view on iOS after a vote for Midnight: each tile shows its share, Midnight at 55 percent and Coral at 45 percent, and the chosen label is underlined.
The grid view after a vote: each tile shows its share (a real screenshot taken in the simulator).
Screenshot of the inline poll view on iOS: the question 'Which colourway next?' and two compact pills, Midnight and Coral, in a short card near the foot of the tale, before a vote.
The inline view before a vote: compact pills in a row (a real screenshot taken in the simulator).
The inline view on iOS after a vote for Midnight: the pills give way to a stacked bar split at 52 and 48 percent, with the legend 'Midnight 52% Coral 48%' under it.
The inline view after a vote: a stacked bar of the shares with a legend, the vote in bold (a real screenshot taken in the simulator).
What you see: the tale opens on the this-or-that poll, a vote for Midnight follows, and the halves move to the vote's share, which then holds for about four seconds. A recording of the sample app (taller), driven by its taller.autoOpenItem and taller.autoVote debug options.

Quiz

Two to four answers. Once answered, the quiz says whether the answer was right: the correct answer is marked with a tick and a wrong one with a cross, as words too. Without a correct answer the quiz works as a survey and shows only the shares. The correct answer comes with the tale, so a quiz is not a secure test.

Screenshot of a TaleArt sample quiz on iOS over a sky picture with clouds: the title Quiz, the question 'What is the capital of France?' and three answer buttons, Paris, Lyon and Nice, before an answer.
A quiz before the visitor answers, in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator.
The same quiz on iOS after the visitor picked Lyon, a wrong answer: Paris is outlined in green with a tick and 73 percent, Lyon is outlined in red with a cross and 19 percent, Nice shows 8 percent.
A wrong answer is marked with a cross and the right one with a tick, each with its share. The marks are read to VoiceOver as words too (a real screenshot taken in the simulator).
What you see: the quiz opens, a wrong answer (Lyon) is picked, the wrong and the right ones are marked with their shares, and the result holds for about four seconds. A recording of the sample app (taller) with its taller.autoVote debug option.

Rating

A slider from 0 to 100 whose thumb is an emoji you choose. The visitor drags it or taps the track, and the rating is sent when they let go. Once rated, the count and the average show.

Screenshot of a TaleArt sample rating on iOS over a night sky with a moon: a dark card titled 'Rate us' with the question 'How was your visit?' and a slider whose thumb is a star, set to the middle, before a rating.
A rating before the visitor sends one, in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator.
The same rating on iOS after the visitor tapped near 90: the star thumb sits near 90 percent of the track, a small mark shows the average at 78 percent, and the line under the track reads 'Average 78 from 213 ratings'.
The rating once sent: the thumb is the visitor's own rating, and the line under the track shows the average of all ratings (a real screenshot taken in the simulator; the tap is a mouse click on the simulator).
What you see: the star is dragged along the track and let go, and the average of all ratings shows. A recording of the sample app (taller) in the simulator; the drag is a mouse gesture on the simulator.

Reaction

Two to six different emoji. A tap sends that emoji; every emoji then shows its count, and the tapped one is selected.

Screenshot of a TaleArt sample reaction on iOS over fireworks on a night sky: a white card titled 'Your reaction' with the question 'How do you feel?' and three emoji buttons, a heart, a laughing face and a surprised face, before a tap.
A reaction row before a tap, in the sample app (taller) on iOS, a real screenshot taken in the iPhone 16 Pro simulator.
The same reaction row on iOS after a tap on the heart: the heart button is outlined as selected and shows a count of 129, and the laughing and surprised faces show 41 and 17.
After a tap the tapped emoji is selected and every emoji shows its count (a real screenshot taken in the simulator).
What you see: the heart is pressed, rises from its button, and every emoji then shows its count. A recording of the sample app (taller) with its taller.autoPress debug option.

Each accepted answer reaches your listener as onUserInteracted, with the element in event.component and the answer as its value, and as onComponentEvent with event.action "selected". An answer kept from an earlier visit is not reported again. When an answer fails, the SDK says so on screen and the visitor can answer again.

Right to left

The SDK's views lay out in the device's direction: Hebrew and Arabic are right to left, so the bar starts on the right, a viewer's progress fills from the right, Previous is the right third of the tale, and the poll and rating layouts follow. Numbers, promo codes and a countdown's digits stay left to right. To set the direction yourself, pass layoutDirection: "rtl" or "ltr"; nil, or any other value, follows the device.

// The layoutDirection argument of TaleartConfig(...) in doInit. Swift needs every argument,
// so pass nil to follow the device's language.
layoutDirection: "rtl",             // or "ltr"; nil (the default) follows the device
Screenshot of the TaleArt sample app's bar on iOS with a Hebrew locale and right-to-left layout: round covers in gradient rings with Hebrew group names under them, the first group on the right and the row running to the left.
The bar with the Hebrew locale and layoutDirection set to rtl, a real screenshot taken in the simulator. The first group is on the right.
Screenshot of the TaleArt sample app's viewer on iOS in right-to-left layout, on the second tale of a Hebrew group: the group name chip on the right, the close and pause buttons on the left, and the progress bars, the first full and the second filling, starting from the right.
The viewer in the same setup, a real screenshot taken in the simulator: the progress fills from the right, and the group name sits at the start of the line.

Backups and the visitor id

The SDK makes a visitor id once and keeps it in its own store on the device: the taleart-sdk NSUserDefaults suite, under the key taleart.visitor. It is random, not derived from the person or the device, and tied to no account. It is still a stable identifier of an app install, so cover it in your privacy notice and your consent setup. Deleting the app makes a new one. iOS backups restore that store too, so a restored device keeps its id. The SDK does not exclude the store from backups, and iOS has no backup rule for a single defaults key. To start fresh after a restore, remove the key once, in the app, when it first runs after the restore: UserDefaults(suiteName: "taleart-sdk")?.removeObject(forKey: "taleart.visitor"). The SDK makes a new id the next time the app starts. Do not remove it on every launch, or every launch gets a new id.

Not available natively yet

  • onGroupImpression counts a group when it is first shown in a render, not when it is on screen: the native SDK does not measure visibility yet.
  • The Compose TaleartZone has no properties parameter: a composable uses the SDK’s viewer properties from TaleartConfig, and cannot set its own for one zone.

The Install a zone page covers the Web SDK, which draws the same elements.