Skip to content

PoC: build-time codegen to SwiftUI for AppIntent reactivity (Track 3)#168

Draft
burczu wants to merge 3 commits into
callstackincubator:mainfrom
burczu:poc/widget-reactivity-track-3
Draft

PoC: build-time codegen to SwiftUI for AppIntent reactivity (Track 3)#168
burczu wants to merge 3 commits into
callstackincubator:mainfrom
burczu:poc/widget-reactivity-track-3

Conversation

@burczu
Copy link
Copy Markdown
Contributor

@burczu burczu commented May 29, 2026

Closes #165

Summary

Proof-of-concept for making Voltra widgets reactive to AppIntentConfiguration parameter changes
via build-time codegen: the config plugin prerenders the JSX component to Voltra JSON, then
translates the JSON tree to a self-contained SwiftUI file. No Voltra SDK at runtime, no JSC, no
bundle copy. Full native SwiftUI reactivity.

The key tradeoff vs the JSC-in-extension approach: layout is baked into the app binary. The server
can push new data values without an app update, but structural layout changes require one.

Developer experience

Same surface as the JSC-in-extension approach — identical JSX and app.json config. The config
plugin detects the appIntent field and routes the widget through the codegen path.

import { Voltra, appIntentParam } from 'voltra'

export const MyWidget = () => (
  <Voltra.VStack style={{ flex: 1, padding: 16 }}>
    <Voltra.Text style={{ fontSize: 22, color: 'primary' }}>
      {appIntentParam('temperature')}
    </Voltra.Text>
  </Voltra.VStack>
)

expo prebuild generates a complete, self-contained VoltraCodegen_<id>.swift — no
import VoltraWidget, no JSC dependency.

What was built

Artifact Location Purpose
swift-codegen.ts packages/expo-plugin/src/ios-widget/files/swift-codegen.ts JSON tree → SwiftUI translator
AppIntentConfig types packages/expo-plugin/src/types.ts appIntent field on WidgetConfig
Config plugin integration packages/expo-plugin/src/ios-widget/files/swift.ts Routes appIntent widgets through codegen
appIntentParam() packages/ios/src/app-intent.ts Developer API (shared with JSC-in-extension approach)
Example widget example/widgets/ios/IosCodegenTemperatureWidget.tsx End-to-end example

The translator covers Text, VStack, HStack, ZStack, and common style props (fontSize, fontWeight,
color, padding, flex). Hex colors are emitted as Color(red:green:blue:) float literals at build
time; unrecognised values fall back to .primary. No runtime string parsing.

{{ appIntent.X }} tokens in the prerendered JSON become entry.X Swift variable references in
the generated view.

Exit criterion — proved in simulator (2026-05-28)

  • AppIntent reactivity: user-configured parameters re-render the widget without a server push ✅
  • Zero Swift required from the developer ✅

Tradeoff vs JSC-in-extension approach

Dimension JSC-in-extension Codegen
AppIntent reactivity ✅ proved ✅ proved
Layout changes without app update ✓ server controls payload ✗ requires app update
Runtime overhead JSC init + ~1 KB bundle eval Zero
Swift required from developer

Gap vs expo-widgets

This approach reaches approximately 10–15% of expo-widgets feature parity. The gap is
architectural: expo-widgets translates JSX → Swift directly, preserving conditional logic and
component composition. This approach goes JSX → Voltra JSON → Swift; the JSON prerender discards
JSX-level structure before the translator sees the tree. Closing the gap would require
reimplementing significant parts of expo-widgets' JSX analysis.

Test plan

  • expo prebuild in example/VoltraCodegen_reactive_codegen.swift generated
  • Verify generated file: no import VoltraWidget, entry.temperature references
  • Build in Xcode — BUILD SUCCEEDED
  • Add reactive_codegen widget in simulator → edit temperature → widget re-renders

burczu added 3 commits May 29, 2026 22:07
…ntained SwiftUI

Adds the first end-to-end step of Track 3 (build-time codegen PoC):

- `appIntentParam(name)` helper exported from voltra and voltra/server
- `AppIntentConfig` / `AppIntentParameter` types on `WidgetConfig`
- `swift-codegen.ts`: JSON tree → SwiftUI translator (Text, VStack, HStack;
  light-dark() hex parsed at build time to Color(red:green:blue:) literals;
  {{ appIntent.X }} tokens → entry.X references)
- `expo prebuild` now writes `VoltraCodegen_<id>.swift` for each widget with
  `appIntent` config — no VoltraWidget SDK import, no JSC, no bundle copy
- Example `reactive_codegen` widget (temperature parameter) with
  IosCodegenTemperatureWidget.tsx + initial state for prerender input
light-dark() color adaptation belongs on Track 1 (rendering improvements),
not Track 3 (build-time codegen). Keeping it here conflated concerns.

- swift-codegen: remove parseLightDark(), simplify colorExpr() to call
  hexColor() directly; remove @Environment(\.colorScheme) from generated
  view struct (no longer needed, would produce unused-variable warning)
- Example widget updated to use 'primary' semantic color
Ensures the compiled plugin build is up to date before expo prebuild runs.
Without this, changes to plugin source (e.g. swift-codegen.ts) are silently
ignored — prebuild uses the stale compiled output in build/cjs/.
@burczu burczu force-pushed the poc/widget-reactivity-track-3 branch from 0c42423 to 27c8680 Compare May 29, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Widget reactivity: support on-device state changes without a server push

1 participant