Your AI can now ship complete native iOS features, not just code snippets. StemJSON is a declarative language describing a full feature — screens, interactions, data, navigation — and StemRuntimeSDK runs it as native SwiftUI on-device. AI authors the feature; users get native iOS.
- Requirements
- Installation
- Quick Start
- Zip-Packaged Modules
- Core API
- State Observation & Events
- Module Lifecycle
- UIKit Integration
- Navigation Embedding
- Custom Repositories
- Custom Services
- Error Handling
- Diagnostics & Logging
- Module JSON
- Thread Safety & Swift 6 Concurrency
- Privacy & Security
- Contributing
- License
| Dependency | Minimum |
|---|---|
| iOS | 18.0 |
| Swift | 6.0 |
| Xcode | 26.0 |
Add the package in Xcode via File › Add Package Dependencies, or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/vkrychun/stem-runtime-swift.git", from: "1.0.0")
]import SwiftUI
import StemRuntimeSDK
struct DashboardView: View {
private let runtime = StemRuntime()
@State private var stemView: AnyView?
var body: some View {
Group {
if let stemView { stemView }
else { ProgressView() }
}
.task {
guard
let url = Bundle.main.url(forResource: "dashboard", withExtension: "json"),
let render = try? await runtime.validate(contentsOf: url).get()
else { return }
stemView = AnyView(render)
}
}
}Three steps in practice: create a runtime, validate a JSON module (file or raw Data), embed the returned render — StemRender conforms to View. The SDK accepts either a single .json file or a zip-packaged module and picks the loader from the byte stream — no flag required.
Use a zip when a module needs bundled assets, localisation, or sub-modules.
my_feature.zip
├── main.json ← required — the module root
├── details.json ← sub-module, loaded via file://details.json
├── localization/
│ ├── en.strings ← "key" = "value"; format
│ └── uk.strings
└── assets/
└── logo.png ← loaded via file://assets/logo.png
- Package resources are referenced with
file://<relative-path>and take precedence over host-app resources with the same path. - A zip without
main.jsonat the root fails validation. .stringsfiles underlocalization/backl10n://sources and thelocalize(key, fallback)expression function. The runtime falls back to the host app bundle if a key is missing.
See StemJSON Specification §14 for the full package format.
The entry point. Create one per app or feature scope.
// Default
let runtime = StemRuntime()
// With diagnostics
let runtime = StemRuntime(.init(enabled: true, minLevel: .warning))Fluent configuration:
let runtime = StemRuntime()
.navigationEmbedded()
.register(MyRemoteRepository.self, as: StemRepositoryType.remote)func validate(data: Data, ignore: [StemIssueSeverity] = []) async -> Result<StemRender, StemValidationReport>
func validate(contentsOf url: URL, ignore: [StemIssueSeverity] = []) async -> Result<StemRender, StemValidationReport>ignore suppresses non-critical severity levels from causing a .failure (e.g. [.warning, .note]).
StemValidationReport conforms to LocalizedError and CustomStringConvertible. Its description is a human- and machine-readable report:
=== Validation Report: 2 errors, 1 warning ===
❌ ERROR | login_btn → onTap | [V002] Value 'repositoryId' is missing
...
The format is designed for AI-in-the-loop authoring: feed the report back to the model and it will revise the StemJSON module until validation passes.
The value returned by validate. It conforms to View, Identifiable, and Equatable, so you can use it in three ways:
// 1. Embed in SwiftUI — StemRender is a View
var body: some View { render }
// 2. Render in UIKit
let vc = runtime.renderViewController(render)
// 3. Read metadata declared in the module's JSON `context`
let title: String? = render.title
let icon: String? = render.iconBeing Identifiable and Equatable makes it safe to use in ForEach and SwiftUI diffing.
let cancellable = runtime.subscribe(to: "cartCount", in: render) { value in
updateBadge(value)
}for await value in runtime.stream(for: "cartCount", from: render) {
updateBadge(value)
}runtime.trigger(event: "themeChanged", data: ["mode": "dark"])The payload is bound into the matching onCustom handler's context. Inside the module JSON, read fields as @{<action.id>.<field>}. Always pass every field the handler needs in the payload — path predicates with @{…} are not supported inside filter values (see StemJSON spec §6.2.1).
A module cannot terminate itself — it only mutates its own state. The host observes a sentinel state key and calls kill:
let cancellable = runtime.subscribe(to: "onClose", in: render) { value in
guard value as? Bool == true else { return }
Task {
await runtime.kill(render)
isPresented = false
}
}kill is a hard termination — the next validate produces a fresh module from initial state. Dismissing without kill preserves state so the next open resumes where the user left off.
let render = try? await runtime.validate(contentsOf: url).get()
let stemVC = runtime.renderViewController(render!)
addChild(stemVC)
view.addSubview(stemVC.view)
stemVC.view.frame = view.bounds
stemVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
stemVC.didMove(toParent: self)Use only when a child view controller is not possible — renderViewController is preferred because it propagates safe-area insets, trait changes, and keyboard avoidance.
let stemView = await runtime.renderView(render)
containerView.addSubview(stemView)
// pin edges with Auto LayoutBy default a module creates its own NavigationStack. When the module is pushed inside a host navigation flow, call .navigationEmbedded() so internal navigation components participate in the host's stack instead:
let runtime = StemRuntime()
.navigationEmbedded()With this enabled, link destinations and navigate push actions land on the host's stack; back-swipe and pop operations sync automatically.
link.destinationmust have"type": "module". Ascroll/vstackplaced there renders but itsevents(notablyonAppear) will not fire. Always wrap pushed layouts as{ "type": "module", "state": {…}, "children": [ … ] }. See StemJSON spec §link.
Do not use .navigationEmbedded() for self-contained modules (tab root, modal presentation) — they manage their own navigation.
Built-in repositories registered automatically:
| Key | Built-in implementation |
|---|---|
StemRepositoryType.remote |
HTTP/REST |
StemRepositoryType.secured |
Keychain-backed secure storage |
StemRepositoryType.local |
On-device document storage |
StemRepositoryType.photos |
Photo library |
Override any of them, or register your own under a custom StemDependencyType:
final class ProductRepository: StemRepository {
typealias Entity = ProductEntity
struct Configuration: Decodable, Sendable { let baseURL: String }
let id: String
let config: Configuration
init(id: String, config: Configuration) throws {
self.id = id
self.config = config
}
func read(_ input: Entity.Read) async throws(StemActionError) -> Entity.Read.Response { /* … */ }
func create(_ input: Entity.Create) async throws(StemActionError) -> Entity.Create.Response { /* … */ }
func update(_ input: Entity.Update) async throws(StemActionError) -> Entity.Update.Response { /* … */ }
func delete(_ input: Entity.Delete) async throws(StemActionError) -> Entity.Delete.Response { /* … */ }
}
runtime.register(ProductRepository.self, as: StemRepositoryType.remote)For streaming sources (WebSocket, Firestore listener, SSE), also conform to StemListenable to back the listen action:
extension ProductRepository: StemListenable {
func listen(_ params: AnyDecodable) -> AsyncThrowingStream<AnyDecodable, Error> { /* … */ }
}Services handle operations outside CRUD semantics — analytics, biometrics, camera, location, deep links, health, and so on. The SDK pre-registers audio (system sounds and haptics) and push (local notifications only — for remote push, register your own implementation). Everything else is a host-provided implementation.
Conform to StemService and implement execute:
final class AnalyticsService: StemService, Decodable {
let id: String
@MainActor
func execute(_ input: Any?) async throws(StemActionError) -> Any? {
// track event, return value for `output.success`, or nil for fire-and-forget
return nil
}
}
runtime.register(AnalyticsService.self, as: StemServiceType.analytics)execute runs on the main actor. Throw StemActionError to trigger the output.failure chain.
For dependencies that don't fit the built-in repository or service categories, define a custom key:
enum AppDependency: String, StemDependencyType { case featureFlags }
runtime.register(FeatureFlagService.self, as: AppDependency.featureFlags)All SDK errors surface as StemActionError, with a typed StemErrorCode and a human-readable message.
let result = await runtime.validate(data: jsonData)
switch result {
case .success(let render): hostView = AnyView(render)
case .failure(let report): print(report.errorDescription ?? report.description)
}Build errors in your own repositories and services with the dedicated initialisers:
throw StemActionError(httpStatusCode: response.statusCode)
throw StemActionError(osStatus: keychainStatus)
throw StemActionError(.network(.notFound), "Product \(id) not found")
throw StemActionError(error, fallback: .unknown)Conform your domain errors to StemActionErrorConvertible to let the SDK translate them automatically:
extension MyDomainError: StemActionErrorConvertible {
func asStemActionError() -> StemActionError { /* … */ }
}StemErrorCode groups codes into GeneralError, NetworkError, StorageError, SecurityError, FirestoreError, and a .custom bridge for your own types.
// Explicit configuration
let runtime = StemRuntime(.init(enabled: true, minLevel: .warning))
// Silence
let runtime = StemRuntime(.init(enabled: false))Defaults match the build: .bingo in DEBUG, .warning in Release. Pass a Diagnostics.Configuration explicitly to override.
Severity levels: .bingo, .info, .note, .warning, .error, .critical.
Messages are emitted through OSLog under the subsystem com.stem.runtime.sdk.
StemJSON modules are a declarative tree: every component has a type, optional context, optional state, and optional children. Values anywhere in the tree may be static, state-bound (${field}), context-bound (@{key}), or expression-evaluated ({{ expr }}).
{ "id": "email_field", "type": "textfield",
"context": { "_label": "Email", "_text": "${email}" } }For the full component catalogue, value syntax, style options, and action types see the StemJSON v1.0 Specification.
Add "version": "1.0" at the module root. The SDK uses it to protect forward compatibility:
| Module vs SDK | Behaviour |
|---|---|
| Same or lower | Renders normally |
| Higher minor | Renders — unknown features show a placeholder |
| Higher major | Validation fails |
Unknown component types never crash the SDK — they render an informational placeholder and their children still display.
The SDK uses Swift 6 strict concurrency. All public types are Sendable.
| Main actor only | Any thread |
|---|---|
Embedding a StemRender in a SwiftUI hierarchy |
StemRuntime() |
renderViewController(_:) / renderView(_:) |
validate(data:) / validate(contentsOf:) |
StemService.execute(_:) |
subscribe / stream / trigger / kill / register |
Custom repositories and services must declare their Configuration and Response types Sendable.
StemRuntimeSDK runs entirely on-device. It contains no telemetry, no analytics, and no phone-home behaviour. The SDK transmits no data to Licensor.
The SDK ships with a PrivacyInfo.xcprivacy
manifest declaring only the iOS required-reason APIs it invokes
on-device.
For your Application, remember to:
-
Add your own
PrivacyInfo.xcprivacydescribing data flows your StemJSON modules cause (Keychain, network requests, etc.). -
Set
ITSAppUsesNonExemptEncryptionin your Application'sInfo.plist. For Apps that only use standard iOS encryption APIs (which covers the SDK's anti-tamper hashing), this is typically:<key>ITSAppUsesNonExemptEncryption</key> <false/>
To report a security vulnerability, see SECURITY.md.
This repository ships StemRuntimeSDK as a pre-compiled binary. SDK
source code is proprietary and is not published here. Bug reports,
documentation fixes, and security disclosures are welcome — see
CONTRIBUTING.md and SECURITY.md.
The StemJSON data format was originated and authored by Vasyl
Krychun and is published separately under the Open Web Foundation
Agreement 1.0 at
github.com/vkrychun/StemJSON.
Distributed under a Proprietary Freeware License. Unlicensed builds display a small "Powered by StemJSON" badge on physical devices — its corner is configurable to fit your UI:
StemRuntime().watermarkPosition(.topTrailing)See LICENSE for the EULA and
THIRD_PARTY_LICENSES.md for the
attribution of embedded open-source components. The StemJSON format
itself — originated and authored by Vasyl Krychun — is governed by
the OWFa 1.0; see the
StemJSON spec repo.
Pricing: stemjson.com/sdk/pricing. Commercial enquiries: [email protected].