iOS reference
The details behind the quickstart for iOS: what the HotLayout Swift package takes, what it does with documents, themes, data, actions, language, pictures and the network, what it reports, and how it decides whether it can read a document. The quickstart is the path from an empty app to a published screen; this page is the reference, ordered by topic. The README of the package is a short introduction.
What each schema minor added is in the contract changelog, and which package version reads which minor is in Which package reads which schema. What changed in each package version is on the releases of the package.
Related pages: quickstart for iOS · widgets · compatibility · placements · localization · theme · data dictionary
The package#
| Contracts it reads | document contract 9.34.0, theme contract 4.0.1, icon set 1.0.0 (the icon drawings are built in) |
| Widgets | all 28 widgets of the contract are drawn |
| The rule | the major version must match: a 9.x reader draws any 9.x document and no 8.x one. A document that uses a field newer than the reader still opens, and the reader skips the field. See Version compatibility |
| Requires | iOS 16 or later, Swift 5.9 or later, no third-party dependencies. macOS 13 is declared only so that the package tests run with swift test; it is not a product platform |
| Install | Swift Package Manager, https://github.com/hotlayout/hotlayout-ios, library HotLayout; the steps are in the quickstart |
| Names | the module is called HotLayout and its types Hotlayout…, on purpose: the names match the other readers of the contract |
| Deprecated | the global functions loadDocument, loadTheme, checkVersion, checkDocumentVersion and resolveLocale still work as deprecated forwards of the members below |
| Licence notices | Apache License 2.0. The icon drawings include icons from Lucide (ISC); the notices are in THIRD-PARTY-NOTICES.md of the package: include them in your app's licences screen |
The public API#
Everything below is in the module HotLayout.
HotlayoutScreen |
the SwiftUI view that draws a document — see Drawing a document |
HotlayoutDocument.load(_:), HotlayoutThemeSet.load(_:) |
read a document or a theme from Data or a parsed JSONValue; throw HotlayoutLoadError — see Loading a document and a theme |
HotlayoutVersions.checkDocument(_:), HotlayoutVersions.check(_:readerVersion:), HotlayoutVersions |
the version rule, without reading the whole document |
LocaleResolution.resolve(_:available:source:) |
which translation, number format and direction a device locale gets |
HotlayoutAction, StandardAction, StandardActions.url(for:allowedDeeplinkSchemes:) |
what reaches onAction, and a helper for the standard actions |
TrackEvent, HapticFeedback, NavigationEvent, ScreenShownEvent, ActionSource |
what the other callbacks receive |
ImagePolicy, WritablePaths, ScreenLabels |
decisions of the host: pictures, writable data paths, texts of the reader |
HotlayoutReport |
what the reader skipped or replaced; never shown to the person |
HotlayoutPlacementView, HotlayoutPlacement, HotlayoutPlacementMap |
placements — see Placements |
Drawing a document#
HotlayoutScreen(document:…) draws a document. The quickstart shows it with the usual callbacks; this table is the whole list.
| Parameter | What you pass |
|---|---|
document (required) |
a HotlayoutDocument read by HotlayoutDocument.load(_:). Read it once, in .task, not on every evaluation of body |
theme |
a HotlayoutThemeSet; HotlayoutThemeSet.builtIn without one |
themeId |
a theme of the set to draw with; the set's default without one |
data |
the values for the document's bindings, a flat map from a data path to a value — see Data |
locale |
the language to show, a locale identifier, passed as is — see Language and right-to-left |
entry |
a named entry point of the document. A name the document does not have opens the initial screen |
timeZone |
the time zone of dates; the device's without one |
now |
the moment countdowns and "days until" count from; the clock without one. With a moment given you own the clock: no countdown ticks |
images |
an ImagePolicy — see Images |
labels |
a ScreenLabels: the few texts the reader says to assistive technology, in your language — see Accessibility |
onAction |
receives every action the reader did not perform itself, with an ActionSource. May be async — see Actions and navigation |
onTrack |
the analytics event of a press (TrackEvent), once per press, before the action |
onHaptic |
the haptic feedback a press asks for (HapticFeedback), before the analytics event; the reader never vibrates by itself |
onNavigate |
asked before the reader changes screens by itself; return false to cancel |
onWrite, writablePaths |
what the user typed or chose, and which paths a document may write — see Writes |
autofill |
false turns every autofill hint of the text fields off |
onScreenChange |
the screen-shown event (ScreenShownEvent: screenId, cause), once per show, the first one included |
onReport |
what the reader skipped or replaced — see What the reader reports |
onScreenCovered |
for a host with its own stack of screens — see Swipe fill |
The reader keeps its own stack of screens and performs navigation inside the document. It draws only the document and takes the size your layout gives it.
Loading a document and a theme#
do {
let document = try HotlayoutDocument.load(documentData)
let theme = (try? HotlayoutThemeSet.load(themeData)) ?? .builtIn
} catch let error as HotlayoutLoadError {
// a document the reader cannot use: show your own screen
}HotlayoutDocument.load(_:) and HotlayoutThemeSet.load(_:) take Data (or a parsed JSONValue) and throw HotlayoutLoadError. The quickstart reads them in .task so that the JSON is read once, and shows ProgressView() until it is: not an empty Group, because an empty view on the first frame never runs .task.
A theme that cannot be read is not a reason to refuse a screen: use HotlayoutThemeSet.builtIn. Where the document and the theme come from is your choice: your backend after a signed webhook, or the published address of the project. See Delivery options.
Version compatibility#
HotlayoutVersions.checkDocument(_:) applies the version rule without reading the whole document, for a host that wants to decide before it loads (and keep the old screen when the new document is too new). HotlayoutVersions.check(_:readerVersion:) takes the version to compare with. The rule is the same in every reader and is described in the compatibility policy: the major must match; a newer minor opens and the reader skips what it does not know.
| Package version | Schema | Theme | Icon set | Platform |
|---|---|---|---|---|
| 0.5.0 | 9.34.0 | 4.0.1 | 1.0.0 | iOS 16+, Swift 5.9+ |
Older package versions and what they read are in Which package reads which schema.
Data#
data maps paths to values, ["user.name": "Maya", "user.points": 1850]. The rules are the same for every reader: a number stays a number, dates are ISO 8601 strings with time and zone, a missing value shows the fallback of the document, and a list draws at most 50 records. Which paths exist is defined by the project's data dictionary; what your backend returns is in the backend guide. The reader takes the values you pass; it never calls your backend.
Language and right-to-left#
Pass the device locale as is: the reader picks the document's language by the contract's rule, formats numbers and dates for it and mirrors the layout for right-to-left scripts. LocaleResolution.resolve(_:available:source:) tells which translation, number format and direction a locale gets. See Localization.
Device language. Locale.current gives the language of the device only if the app declares the languages it is localized for (the app's localizations in the Xcode project, or CFBundleLocalizations in Info.plist). Without that iOS reports English, and a document that has an Arabic translation stays English on an Arabic device. Declare the languages of your documents, or pass Locale.preferredLanguages.first as locale.
Actions and navigation#
- Navigation inside a document (
navigate,navigate.back,navigate.section) is done by the reader;onNavigatemay cancel it by returningfalse. openUrl,deeplink,closeand the actions of your app go toonAction. The reader opens nothing itself. A standard action arrives checked against the forms of the contract, andStandardActions.url(for:allowedDeeplinkSchemes:)checks it again before you open it (the form of the address, the scheme of a deeplink against the allowed ones), so an action assembled by hand is not trusted because of its type.- An
onActionmay beasync. A button that confirms by a swipe then stays busy until the handler returns; with a plain handler it stays busy until the screen changes, or eight seconds:
HotlayoutScreen(document: document, onAction: { action, source async in
if action.type == "redeem" { await redeem(source.record?["item.id"]?.stringValue) }
})source.record is the record of the node that was pressed: a row of a list carries its own record, so one handler serves every row.
Swipe fill#
A swipe button with confirmTransition: 'ripple' that leads to another screen fills the screen with its colour before the screen changes. By default the fill lies over the view of HotlayoutScreen and the reader changes its own screens under it. If your app keeps its own stack of screens (you cancel the change with onNavigate and show your own view), pass onScreenCovered: the fill then lies in a layer over the whole window, above your views, so it outlives the screen the button was on. The handler gets true when a fill starts to cover the screen, before the action reaches onNavigate or onAction (change your screen then, without a transition of your own), and false once the last fill is gone. With no window to draw in there is no fill and the action runs at once.
Writes#
Fields, setValue and toggleValue write to the session of the screen and to onWrite, with a string, a flag or a number. WritablePaths says which paths a document may write; by default every path of the form of a data path is allowed. A path that is refused is written neither to the session nor to the host.
Images#
Images load only from an address of the form of an openUrl address (https: and a public host name) by default. Pass an ImagePolicy as images to decide yourself:
ImagePolicy |
|
|---|---|
resolve |
your decision about an address; your answer is final, except that javascript: and vbscript: are never a picture |
| asset tokens | addresses for logo, banner and placeholder |
maxDownloadBytes |
the largest download, 10 MiB by default |
maxPixelSide |
4096 by default: a larger picture is made smaller, not refused |
A refused address makes no request and goes to onReport.
Fonts#
Fonts are the app's: the package ships none. A family the theme names that the device lacks is drawn with the system font.
Accessibility and labels#
The few texts the reader says to assistive technology (the name of the back arrow) are not in the document. Pass ScreenLabels in the language of your app as labels; the defaults are English.
What the reader reports#
onReport receives a list of HotlayoutReport: what the reader skipped or replaced in the document, the theme or while drawing, for your log or analytics. Each report is handed over once, as it appears. It is never shown to the person.
- Documents, themes and data are untrusted input. A piece of the wrong shape is skipped and reported, never a crash. A tree deeper than 32 levels is refused.
- Lists draw at most 2550 instances of list templates per screen, nested lists included; what is beyond is not drawn and is reported once.
- Haptic feedback is asked of the host (
onHaptic); the reader never vibrates.
The trust boundary and the tests that pin it are described in the security notes of the package.
Placements#
A placement is a named place of your app (home.banner). The app names the place in code; which published document stands there is decided in the admin, and changed without a release. The files and the rules of the map are in Placements; this part is what the iOS reader does with them.
// Draws what the admin put in the place `home.banner`, and nothing when it is empty.
HotlayoutPlacementView(
name: "home.banner",
projectBase: projectBase,
onPlacement: { placement in
if let report = placement.report { print("home.banner:", report, placement.mapStatus) }
}
)
/// Without a view: load the place, then draw the document yourself.
func loadBanner(projectBase: URL) async -> HotlayoutDocument? {
let placement = await HotlayoutPlacement.load(name: "home.banner", projectBase: projectBase)
return placement.document // `placement.entry` is the entry point to pass to `HotlayoutScreen`
}The reader asks placements/published.json under projectBase with its ETag, and the document of the place under documents/{documentId}/published.json. It keeps the last map and document it read, and falls back, in this order, on the last known map and then on bundledMap (the text of a copy shipped with the app, for a first start without a network). Nothing throws; a place that draws nothing says why in placement.report:
report |
|
|---|---|
empty |
the place is declared and empty on purpose |
unknown |
the map has no such place (or the project published no map) |
unreadable |
the map, or this place's record, could not be read |
documentMissing / documentUnreadable |
the document is gone (404) / could not be read |
entryMissing |
the entry point is not in the document: it is drawn from its first screen |
placement.mapStatus says why that map is in use: fresh, offline (the network was silent), corrupt (it answered with something that is not a map), unsupported (a map of another major version) or notPublished (404).
- A name is lowercase Latin letters and digits in dot-separated segments; a name of another form is
unknownand no request is made for it. - Use
HotlayoutPlacementView(name:projectBase:…content:)to draw with aHotlayoutScreenof your own configuration. store:(the last known map),documents:(the documents read; files in the caches directory by default) andsession:choose where the last known copies live and how the requests are made.- Redirects are not followed and a request is given up after 20 seconds.
- The view loads the map once per name; it does not poll.
onPlacementis called with the first state and again wheneverplacement.event(status,report,mapStatus,documentId,entry) changes.
Schedule#
A place may carry a schedule: promotions {documentId, entry?, from, until} shown over its permanent content while from (included) to until (excluded) lasts, in UTC. An item with documentId: null is a pause: the place is empty (report == .empty) for its window. After the last window the permanent content is back.
The schedule is resolved by the clock of the device, without a network. The view sets a timer for the next edge of the schedule and, when it fires, resolves the place again: it does not ask for the map again, and only reads the document the new window brings. It also resolves again when the app returns to the foreground, because the clock may have been set. The change is an ordinary onPlacement event (documentId, entry, and empty to document and back); if the answer is the same, there is no event.
HotlayoutPlacement.load(…, clock:) and HotlayoutPlacementView(…, clock:) take the clock (@Sendable () -> Date, the device clock by default), which is how you test a schedule. HotlayoutPlacementMap has resolve(_:at:) and nextBoundary(_:after:) to ask a map yourself.
An empty place draws nothing, but the view stays in your layout, and so do the padding and the frame you put around it. Take your own out when the place is empty, and keep the view mounted so that it can wake up on the next window:
HotlayoutPlacementView(
name: "home.banner",
projectBase: projectBase,
onPlacement: { placement in empty = placement.document == nil }
)
.padding(.horizontal, empty ? 0 : 16)
.padding(.vertical, empty ? 0 : 8)A reader older than the schedule shows the permanent content of a place and ignores its schedule.
Limitations#
- Where Apple's platform differs from the other readers is listed in the platform notes of the package.
- Fonts are not shipped (see Fonts).
- A list draws at most 2550 template instances per screen (see What the reader reports).
Next#
- Quickstart for iOS — the path from an empty app to a published screen.
- Widget reference — what each widget takes.
- Compatibility policy — what a reader does with a document newer than itself.