App8Cloud is the iOS SDK for the App8 platform. It wraps
the App8Engine rendering engine
(its repository is named app8-ios-sdk) and adds everything needed to deliver
dynamic UI from the App8 API:
- A built-in
App8DataSourcethat fetches DSL over/sdk/v1/. - Token-based auth (
app8_live_…/app8_test_…). - An on-disk cache scoped per
(appId, screenId, version). - Native fallback closures for resilience when a render fails.
- Host observability — subscribe to DSL
.emitaction events, observe analytics events (auto-fired and author-declared), and override the active translation locale. - Render telemetry callbacks.
If you need full control over how DSL is fetched (custom API, offline-only),
use App8Engine directly with your own App8DataSource.
Status — pre-1.0. The API may change between
0.xreleases; breaking changes bump the minor version. SemVer guarantees begin at1.0.
- iOS 18+ — the minimum is shared with the App8Engine rendering core, and
the SDK is written for Swift 6 strict concurrency (
@MainActor-isolated throughout). - Swift 6.1+
Swift Package Manager:
.package(url: "https://github.com/appeight/app8-ios-cloud-sdk.git", from: "0.2.2"),Then add App8Cloud to your target's dependencies.
Create one instance and keep a strong reference to it — it owns the cache and
the rendering engine, so hold it on your app or scene delegate (not a local
variable). All App8Cloud APIs are main-actor isolated.
import App8Cloud
guard let apiURL = URL(string: "https://your-api.example.com/sdk/v1") else { return }
let cloud = App8Cloud.instance(
token: "app8_live_abc123xyz", // your App8 SDK token
appId: "com.partner.flagship", // your App8 app identifier
environment: .custom(apiURL)
)
// Optional: identity attributes for analytics dashboards.
cloud.setAttributes([
"userId": MyAuth.currentUserId ?? "",
"plan": MyAuth.currentPlan ?? "free",
])screen(...) is async and never throws — it renders your fallback view
controller on failure. Call it from a Task:
// UIKit
Task {
let vc = await cloud.screen(id: "home", version: nil, parameters: [:]) { error in
NativeHomeViewController(error: error)
}
present(vc, animated: true)
}A throwing variant — try await cloud.screen(id:version:parameters:) — is also
available if you'd rather handle errors yourself.
SwiftUI handles the async load for you — pass a placeholder and a fallback:
App8CloudScreenView(
instance: cloud,
screenId: "home",
placeholder: { ProgressView() },
fallback: { error in NativeHomeView(error: error) }
)If the device is offline on first launch with nothing cached, the render fails and your fallback is shown; once a screen is cached it renders without network.
The first screen(...) call makes several sequential network round-trips
(manifest, styles, components, screen). Warm the cache at launch so the first
navigation is instant:
Task.detached(priority: .background) {
await cloud.prefetchAll() // discovers and warms every screen + assets
}prefetchAll() fetches the app manifest, styles, components, every reachable
screen, and the asset manifest. To warm only specific screens instead:
await cloud.prefetch(screens: [.init(id: "home"), .init(id: "settings")])Preloading is idempotent (cached entries short-circuit), best-effort
(per-screen failures don't abort the batch), and cancellable via
cloud.cancelPrefetch().
App8Cloud is the client half — it fetches and renders screens, it does not
create them. A "screen" is a DSL document (JSON) served by the API over
/sdk/v1/. Authoring tooling is outside the scope of this package.
DSL screens signal the host app rather than calling native code directly: a
control fires a URL and your app handles it, deep-link style. The App8SDKDemo
sample shows a host catching DSL-fired openURL events and pushing a native
screen in response.
App8Cloud.Instance exposes the engine's two in-process buses directly so
your host can react to what users do inside DSL screens and route analytics
to your own tracker (Mixpanel, Amplitude, Segment, …).
DSL authors can fire named events from any component via the .emit action
(e.g. connect.tapped, user.selected). Subscribe by name, by screen,
or to everything:
let sub = cloud.subscribe(to: "connect.tapped") { event in
MyAnalytics.track("connect_tapped", properties: event.payload)
}
// Hold `sub` for as long as you want events; let it deallocate to unsubscribe.Combine and AsyncStream variants are available as cloud.events and
cloud.eventStream. To register a single handler delegate-style, use
cloud.setEventHandler(_:) (the handler is held weakly).
The engine auto-fires app8.* lifecycle events — app8.screen.appeared,
app8.screen.dismissed, app8.component.tapped, app8.navigation.pushed,
app8.url.opened. The cloud SDK adds four more to the same bus:
app8.screen.rendered— success arm, fires when ascreen(...)/startApp(...)render succeeds. Funnel denominator.app8.render.failed— render failed.app8.render.fallback— host-supplied fallback ran after a failure.app8.screen.shortcircuit— availability precomputed; the cloud short-circuited the request.
Author-declared analytics events (set via an analytics JSON binding in the
DSL) flow on the same bus. Author names get app8. prepended automatically
at dispatch — stripeConnectClicked lands as app8.stripeConnectClicked.
The canonical pattern is one handler that proxies every event to your real
tracker — event.properties is the fully-merged payload (SDK injects
screen_id, engine_version, cloud_version, etc. as snake_case keys):
final class App8ToMixpanel: App8AnalyticsHandler {
func app8DidTrack(_ event: App8AnalyticsEvent) {
Mixpanel.mainInstance().track(
event: event.name,
properties: event.properties
)
}
}
cloud.setAnalyticsHandler(App8ToMixpanel()) // held weaklycloud.observeAnalytics(_:), cloud.analytics (Combine), and
cloud.analyticsStream (AsyncStream) are also available.
Tune which auto-events fire by mutating cloud.analyticsConfig —
autoScreenEvents, autoComponentTaps, autoNavigationEvents,
autoUrlEvents, autoCloudEvents (all default true). The cloud SDK's
four render-lifecycle events are gated by autoCloudEvents. The engine's
screen lifecycle events stay on their own autoScreenEvents toggle.
To strip PII at the bus boundary (e.g. query strings on app8.url.opened),
set cloud.analyticsBus.redact — see docs/dsl/analytics.md
in the engine repo for the contract.
Privacy note. Both buses are entirely in-process. Nothing on these buses is transmitted to App8's servers — that path is the separate, opt-out remote telemetry (
telemetry: .disabled).
The engine uses the device locale for {"$i18n": …} lookups and
locale-aware formatters by default. Override it:
cloud.setLocale("fr-FR") // force French
print(cloud.currentLocale) // "fr-FR"
cloud.setLocale(nil) // revert to device default- Telemetry — the SDK sends anonymous render/usage telemetry to the API
by default. Pass
telemetry: .disabledtoApp8Cloud.instance(...)to turn it off. The SDK mints no device identifier. - Identity — attributes you pass to
setAttributes(_:)are sent to the API for analytics; only what you set is transmitted. - Privacy manifest — the package ships
PrivacyInfo.xcprivacy. - App Store review — dynamic UI is permitted under App Review Guideline 4.7. Don't ship new permissions or core functionality via DSL.
- Token —
app8_live_…is a publishable client token, not a secret; it is embedded in your app binary. Security comes from server-side scoping and rate-limiting, not token secrecy.
The App8 iOS repository includes two runnable partner-style hosts —
App8CloudDemo (minimal) and App8SDKDemo (embeds a cloud screen as a child
view controller) — useful as end-to-end integration references.
Report bugs and ask integration questions via GitHub Issues on this repository.