ReviewOS

stacks/stx

feat(stx-native): vertical slice — one wildloop screen rendering real UIViews on the simulator

#1987
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Summary

One wildloop screen, rendered as real UIViews on the simulator, with one working interaction. Not navigation, not lists, not Android. The single question this answers: does the architecture work end to end?

Why this shape

packages/stx-native has renderers for both platforms (ios.swift 765 lines, android.kt 1,305) and a bridge protocol, and none of it has ever rendered anything. Until IR reaches a device and a signal update changes a pixel, every other estimate in this effort is a guess.

The slice

  1. A wildloop screen in .stx — static layout, one label bound to a signal, one button.
  2. stx renders it; the translator produces IR (see the parser issue).
  3. ios.swift materialises the IR as a real view hierarchy inside a Craft shell.
  4. Tapping the button calls a handler over the bridge; the signal updates; the UILabel changes.

Explicitly out of scope

Navigation, lists and cell recycling, Android, hot reload, gestures, the component library. Each is a separate issue and each is cheaper to scope once this one has run.

Done when

  • The screen renders on the simulator with no WebView in the hierarchy.
  • Tapping the button updates the label.
  • The round trip from tap to repaint is measured and recorded in the issue.
  • Everything that had to be stubbed or faked is written down — that list is the real project plan.
9 days ago

Part of #1992 (stx-native: native views for wildloop).

9 days ago

I could not run the slice, but I de-risked the two things that would have sunk it and found the blocker list the issue asks for. 32c6075.

The blocker

ios.swift imports YogaKit and uses it in 44 places — all layout goes through configureYogaLayout. It is not vendored, not in any manifest, and not fetchable from anything in this repo. There is no Package.swift for the renderer and the only Xcode project here, examples/buddy-ios, is a CraftWebView — the WKWebView wrapper #1992 is about, which does not reference the renderer at all.

So "materialise the IR inside a Craft shell" has no shell to go in yet. That is the first item on the plan, not a detail.

What I verified instead, since it was cheap and decisive

1. The Swift parses. swiftc -parse ios.swift is clean. Given #1985 was a committed syntax error in the CLI, "has never been executed" made this worth asking.

2. The IR actually decodes into the Swift structs. This is the contract between the working front half and the never-run back half, and nothing had ever tested it. I extracted the Codable mirror out of ios.swift, compiled it against Foundation, and fed it the real output of parseSTXToNode:

$ irdecode < ir.json
DECODED ok: type=View children=1
  node: Text
    text: Wildloop

The reference tree round-trips. DimensionValue handles width: "100%" alongside height: 40 correctly. The wire format is sound — which is the single best piece of news available before a device exists.

The finding that matters for the plan

A component type with no renderer branch at least falls through a switch. A style property the compiler emits and a renderer does not read is simply absent: Swift's Codable ignores unknown keys, so it decodes into nothing, no error anywhere, and the view lays out as though the class had never been written.

style properties
IR declares80
android.kt refers to68
ios.swift declares52

iOS silently drops 28 — a third of the vocabulary — on top of the three component types it is already missing (FlatList, Modal, Slider, from the earlier coverage test). Among the dropped: lineHeight, letterSpacing, fontFamily, overflow, zIndex, transform, aspectRatio, flexBasis, textTransform, and every per-edge border property.

That is pinned now, in both directions, so the list can only shrink.

The plan this produces

In order, each a prerequisite of the next:

  1. A layout dependency and a shell. Vendor YogaKit or replace it; add a Package.swift or an Xcode target that compiles ios.swift for the simulator. Until this exists nothing else can be run.
  2. Close the 28 style gaps and the 3 component gaps, or decide explicitly which are out of scope and make the renderer say so rather than drop them silently.
  3. The JS side. The CLI can emit a JavaScriptCore bundle; the signal→repaint round trip needs that bundle driving the host from #1984 — which is now implemented and tested, so the mutations have somewhere to go.
  4. Then the slice: screen, tap, label, and the measurement this issue asks for.

I would rather hand that over than report a screen that rendered because I stubbed the layout engine. Steps 1 and 3 are also the two places the remaining estimate actually lives.

9 days ago

Completed on main, including the simulator-only done criteria.

The vertical slice is examples/buddy-ios: Home.stx composes CounterCard.stx, Craft evaluates the generated bundle in JavaScriptCore, creates UIKit views with its real native flex layout engine, and applies revisioned stx signal mutations. This is not the old WebView example and does not stub layout.

The isolated NativeRenderUITests target passed 1/1 on an iPhone 17 Pro simulator running iOS 26.5. It asserted that no WKWebView exists, typed Ada into the native text field, observed Hello Ada, tapped the native button three times and observed Count: 1, Count: 2, and Count: 3, while focus and the on-screen keyboard remained intact.

The saved XCTest activity trace records the requested round trip. From the end of each synthesized tap (app idle) to the successful updated-label lookup: 281 ms, 268 ms, and 265 ms, median 268 ms. This is deliberately documented as an XCTest upper bound because it includes synchronization and accessibility lookup overhead, not as pure bridge latency.

Relevant commits: d0cf560753, 4331162b32, fc15df7077, fe46011ee6, b52ea061ab, 05a6e1966f, 8d26ab0e1a, 43c9316d7e.

Verification: serialized repository suite 15,224 pass / 0 fail; CI run 38051048775 green across lint, typecheck, tests, coverage, integration and publish. Supported and unsupported behavior, renderer ownership, and the measurement caveat are recorded in the native guide and architecture document.

Sign in to comment on this issue.