Appendix C — iOS / SwiftUI priors that fail in StemJSON
StemJSON is declarative and closed. LLMs trained on UIKit, SwiftUI, and React Native code routinely reach for patterns that look right but fail validation or render verbatim. This appendix names the wrong patterns explicitly — paired with the StemJSON equivalent — so the model has a direct mapping rather than having to infer from absence.
Add to this list whenever a new class of recurring mistake appears in generated output.
Colors
Section titled “Colors”See §7.3 anti-patterns. Do NOT use UIKit semantic color names (label, secondaryLabel, tertiaryLabel, quaternaryLabel, systemBlue, systemRed, systemGray, systemBackground, etc.). Use the §7.3 closed list of supported keywords.
| Wrong (iOS instinct) | Right (StemJSON) |
|---|---|
"foreground": "label" | "foreground": "primary" |
"foreground": "secondaryLabel" | "foreground": "secondary" |
"foreground": "tertiaryLabel" | "foreground": { "color": "primary", "opacity": 0.6 } |
"background": "systemBackground" | "background": "background" (semantic) |
"background": "secondarySystemBackground" | "background": "surface" |
"foreground": "systemBlue" | "foreground": "accentColor" or "blue" |
Padding
Section titled “Padding”- ❌
"padding": { "all": 16 }— noallkey - ✅
"padding": 16(single number = all sides) - ✅
"padding": { "top": 16, "bottom": 16, "leading": 16, "trailing": 16 } - ✅
"padding": { "horizontal": 16, "vertical": 12 }
- ❌
"font": { "size": 18, "weight": "semibold", "design": "rounded" }— nosizeoutside{ "kind": "system", ... }, nodesign, notracking, noleading - ✅
"font": "headline"— preset string (see §7.2) - ✅
"font": { "kind": "title2", "weight": "semibold" }— named kind + weight - ✅
"font": { "kind": "system", "size": 18 }— only thesystemkind takessize
Layout dimensions
Section titled “Layout dimensions”- ❌
"layout": { "frame": { "maxWidth": "infinity", "maxHeight": "infinity" } }— noframewrapper - ✅
"layout": { "maxWidth": "infinity", "maxHeight": "infinity" }— dimensions live directly underlayout(see §7.6) - ❌ Pinning primary content to fixed
width/heightin points on a non-scrolling root (e.g. a 300×300 dial that fills the screen) — overflows smaller viewports and large Dynamic Type, and the runtime clips the overflow (see §4.6). Size it responsively, or put the screen in ascroll.
Stack spacing / alignment
Section titled “Stack spacing / alignment”- ❌
"style": { "layout": { "spacing": 24, "alignment": "leading" } }on avstack/hstack - ✅
"context": { "_spacing": 24, "_alignment": "leading" }— stacks take spacing/alignment in their context, not style
Button content
Section titled “Button content”- ❌
"context": { "_text": "Save" }on abutton—_textis the content key fortextcomponents only - ✅
"context": { "_label": "Save" }— buttons use_label - ✅ Or omit
_labeland providechildrenfor custom button content
Custom event names
Section titled “Custom event names”- ❌
"events": { "onSave": [...], "onIncrement": [...] }— there are no developer-named events; the event vocabulary is closed (see §9.1) - ✅ Wire actions directly to the standard event the gesture/lifecycle fires (
onTap,onSubmit,onAppear, etc.)
Action references
Section titled “Action references”- ❌
"events": { "onTap": "handleSave" }—onTapis an array of action wrapper objects, never a string handle to a named handler - ✅
"events": { "onTap": [ { "state": { "id": "...", "input": { ... } } } ] }— inline action array
Format descriptors
Section titled “Format descriptors”- ❌
"_format": "date"— bare-string_formatis silently ignored, raw double renders (see §4.3) - ❌
"_format": { "style": "date" }— missing formatter-kind wrapper - ✅
"_format": { "date": "friendly" }— single-key wrapper, value is style string or dict
SwiftUI modifiers
Section titled “SwiftUI modifiers”- ❌
.padding(),.foregroundColor(),.font(),.cornerRadius()— there are no modifiers - ✅ Everything is declarative
style.*blocks on the component
Programmatic color refs
Section titled “Programmatic color refs”- ❌
"Color.blue","UIColor.systemBlue",".systemBackground","Color(red: 0.2, green: 0.5, blue: 0.8)" - ✅ Bare keyword from §7.3 closed list, or hex literal
SF Symbol names
Section titled “SF Symbol names”- ❌
"sf:plus.circle","plus.circle"as an_image/_sourcevalue - ✅
"sf://plus.circle"— fullsf://scheme prefix required everywhere except the_tabImageexception (see §6.4)
Background gradient
Section titled “Background gradient”- ❌
"background": { "linear": { ... } }— bare gradient without thegradientdiscriminator wrapper - ✅
"background": { "gradient": { "linear": { ... } } }— see §7.5
Picker option objects
Section titled “Picker option objects”- ❌
"_options": [ { "label": "Small", "value": "s" }, { "label": "Medium", "value": "m" } ]— pickers take a flat array of primitive values, not labeled objects - ✅
"_options": ["small", "medium", "large"]— see §4.4 picker
Module wrapping
Section titled “Module wrapping”- ❌
{ "module": { "id": "...", "type": "module", ... } }— no wrapper key - ❌
{ { "id": "...", "type": "module", ... } }— no double brace - ✅ The outermost
{IS the module’s opening brace:{ "id": "...", "type": "module", "version": "1.0", ... }
Merged marker prefixes
Section titled “Merged marker prefixes”- ❌
"_text": "{{ @${forecast}.temperature }}"—@$is not a marker prefix. The runtime parses the whole string as a literal and renders it verbatim, curly braces included. - ❌
"_text": "@${path}","_text": "${@{ctx}}","_text": "#{${state}}"— every merged form is invalid; markers do not nest or combine. - ✅ Pick exactly one marker per reference based on where the data lives:
- state →
${forecast.temperature}or{{ ${forecast.temperature} }} - context →
@{forecast.temperature}or{{ @{forecast.temperature} }} - system →
#{now}or{{ #{now} }}
- state →
Most common AI-authoring trigger: the model has seen examples using both ${} and @{} and “helpfully” combines them. There is no combined form; each marker maps to exactly one resolution scope. If you don’t know whether the data is in state or context, look at where it was last written (state action target vs. context-key declaration) and use the matching marker.
Navigation source for a sibling screen
Section titled “Navigation source for a sibling screen”- ❌
"navigate": { "input": { "operation": "push", "source": "file://detail.json" } }in a single-file inline module —file://resolves inside a zip package; in a single-file module there IS no file nameddetail.json, so the push silently fails (no transition, no error). - ❌
"source": "detail"— bare string, no scheme prefix; rejected as not a source string. - ✅ Declare the destination as a
componentdependency, then push viadp://:"dependencies": [{ "component": { "id": "detail", "type": "module", "children": [ /* … */ ] } }],"events": { "onTap": [{ "navigate": { "id": "go", "input": { "operation": "push", "source": "dp://detail" } } }] }
This is the same class as the “wrapper-required” mistakes elsewhere — the spec accepts two source schemes in navigate.input.source but only one of them is meaningful for inline modules. Authoring AI tends to pick whichever LOOKS most natural (“file://detail.json” reads like a file reference) without understanding that single-file modules have no file system to reach into.