How Dactyl works · Dactyl
 · August 26, 2026
App development has always been harder than web development for LLMs. Apps tend to have platform style norms and layouts, and they need to look and feel a certain way. People can feel when an app is “native” and crafted for their device. Plus, the web is open and universal in a way apps and app stores typically are not, which means LLMs have comparatively less data in their training sets.
Many vibe-coding platforms have tried to solve the problem by, essentially, compromising. They generate apps using Expo/React Native, Flutter, or even just webviews. But we found none of those approaches provides an optimal user experience; you're always sacrificing something, whether it's cross-platform compatibility, native widgets, performance, or features.
**We made Dactyl to help you build native apps without those compromises**.
Today we present the inner workings of Dactyl’s cross-platform SwiftUI renderer—a Wasm port of the iOS simulator—and how we bring the Apple ecosystem to Android and the web.
iOS previews on the web
We chose Swift as the primary language in Dactyl because of its simplicity and well-defined layout semantics. In fact, Dactyl was initially designed to be a development platform exclusive to iOS.
Previewing an iOS app without a Mac normally means renting one and streaming a real simulator back: a boot, an install, and a launch before you even _see_ the app, metered for as long as you keep working.
That approach only scales as far as your budget allows. But what if you could have a simulator inside your browser? Well, that would scale infinitely. So we built it.
Dactyl’s preview engine is a reimplementation of SwiftUI in Swift, plus the frameworks apps reach for. Plenty of modules are implemented, including Charts, SpriteKit, SceneKit, RealityKit, ARKit, MapKit, StoreKit, MusicKit, PencilKit, WidgetKit, UIKit, CoreGraphics, Metal and Vision.
Apps are built to Wasm and linked against our SwiftUI instead of Apple’s.
The engine emits a binary stream of draw commands into its own linear memory, and the protocol is read by a JavaScript host that paints it on a Canvas2D.
Layout is fully resolved before any command is written, and a reconciler keeps the view tree between passes, marking dirty only the views that read something live this frame: animations, scrolling containers and the like. The host rasterizes each layer once onto an offscreen canvas keyed by a hash of its contents, then composites on later frames.
All of this means Dactyl can render complex SwiftUI layouts smoothly and efficiently, directly in the browser, without any installation step or extra devices. (But it can handle all of that, too.)
_Layout tree_ 293 nodes · depth 23
- Fill 402×874
* _Layer 402×778 * Decoration 402×778
- Fill 402×778
* Frame 402×778 * Stack 402×778 * Frame 402×647 * _Layer 402×647 * Scroll 402×647 * Padding 402×1143 * Stack 402×1119 * _Layer 402×87 * Padding 402×87 * Padding 402×75 * Stack 370×75 * Stack 370×31 * _Layer 30×30 * Frame 30×30 * ZStack 30×30
- Fill 30×30
* Stack 19×18 * Frame 3×9
- Fill 3×9
* Frame 3×16
- Fill 3×16
* Frame 3×12
- Fill 3×12
* Frame 3×18
- Fill 3×18
* Frame 3×10
- Fill 3×10
- Text 167×31
- Spacer 72×0
- Symbol 21×22
- Symbol 22×22
- Symbol 22×22
* Stack 370×30 * Button 44×30 * Decoration 44×30
- Fill 44×30
* Padding 44×30 * Padding 44×16
- Text 16×16
* Button 65×30 * Decoration 65×30
- Fill 65×30
* Padding 65×30 * Padding 65×16
- Text 37×16
* Button 85×30 * Decoration 85×30
- Fill 85×30
* Padding 85×30 * Padding 85×16
- Text 57×16
- Spacer 160×0
* Padding 402×232 * GridFlow 370×232 * _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 170×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 110×29
- Spacer 0×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 159×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 99×29
- Spacer 0×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 181×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 36×14
- Spacer 85×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 176×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 116×29
- Spacer 0×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 178×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 118×29
- Spacer 0×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 181×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 33×14
- Spacer 88×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 171×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 111×29
- Spacer 0×0
* _Layer 181×52 * _Tap 181×52 * Clip 181×52 * Decoration 181×52
- Fill 181×52
* Frame 181×52 * Frame 181×52 * Stack 181×52 * _Layer 52×52 * Clip 52×52 * Clip 52×52 * Frame 52×52 * AspectRatio 52×52
- RemoteImage 52×52
- Text 120×29
- Spacer 0×0
* _Layer 402×235 * Stack 402×235 * Padding 165×25
- Text 133×25
* Scroll 402×197 * Padding 1166×197 * Stack 1134×197 * _Layer 150×197 * _Tap 150×197 * Frame 150×197 * Stack 150×197 * _Layer 150×150 * Clip 150×150 * Clip 150×150 * Frame 150×150 * AspectRatio 150×150
- RemoteImage 150×150
- Text 147×17
- Text 50×14
* _Layer 150×197 * _Tap 150×197 * Frame 150×197 * Stack 150×197 * _Layer 150×150 * Clip 150×150 * Clip 150×150 * Frame 150×150 * AspectRatio 150×150
- RemoteImage 150×150
- Text 147×17
- Text 50×14
* _Layer 150×197 * _Tap 150×197 * Frame 150×197 * Stack 150×197 * _Layer 150×150 * Clip 150×150 * Clip 150×150 * Frame 150×150 * AspectRatio 150×150
- RemoteImage 150×150
- Text 119×17
- Text 50×14
* _Layer 402×235 * Stack 402×235 * Padding 185×25
- Text 153×25
* Decoration 402×131 * IgnoresSafeArea 402×131
- Fill 402×165
* Stack 402×131 * Padding 402×68 * Padding 402×64 * _Layer 402×58 * _Tap 402×58 * Padding 402×58 * Clip 386×58 * Decoration 386×58
- Fill 386×58
* Stack 386×58 * Padding 386×56 * Padding 386×40 * Stack 366×40 * _Layer 40×40 * Clip 40×40 * Clip 40×40 * Frame 40×40 * AspectRatio 40×40
- RemoteImage 40×40
* Stack 94×31
- Text 94×16
- Text 65×13
- Spacer 155×0
* Padding 23×20
- Symbol 17×20
* Button 34×34 * Frame 34×34
- Symbol 18×21
* Padding 386×2 * Frame 370×2 * GeometryReader 370×2 * ZStack 370×2
- Fill 370×2
* Frame 0×2
- Fill 0×2
* _Layer 402×63 * Padding 402×63 * Padding 402×53 * Stack 402×43 * Button 134×42 * _Layer 134×42 * Frame 134×42 * Stack 31×42
- Symbol 31×26
- Text 29×12
* Button 134×41 * _Layer 134×41 * Frame 134×41 * Stack 34×41
- Symbol 26×25
- Text 34×12
* Button 134×43 * _Layer 134×43 * Frame 134×43 * Stack 60×43
- Symbol 31×27
- Text 60×12
the frame it resolves to
Dynamic linking Swift
SwiftWasm links statically. That is not ideal for us, because the engine is ~100 MB and a static link means linking and shipping hundreds of megabytes to the browser on every edit.
The app itself is much smaller, so we split the build. The engine and the standard library link once as a base module, content-addressed, so the browser fetches and compiles it one time and keeps it across edits.
Your app compiles on its own as a position-independent side module, 8 KB to 300 KB. At load, it’s placed at a runtime memory offset, its imports are bound to the base’s exports, and its Swift type metadata is registered with the base’s runtime. The swap happens under the running app. `@State`is snapshotted before and restored after, so scroll position, text fields, and app data are preserved across edits.
examples/weather/ContentView.swift _79_]_80_ _81_ private let columns =[GridItem(.flexible(), spacing: 14),_82_ GridItem(.flexible(), spacing: 14)]_83_ _84_ var body:some View{_85_ ZStack{_86_// Sky: a top→bottom blue gradient behind everything._87_ LinearGradient(_88_ colors:[Color(red: 0.21, green: 0.50, blue: 0.86),_89_ Color(red:0.42
36 KB 0.28 s
Android: Jetpack Compose
On Android, the engine compiles natively for `aarch64-unknown-linux-android24`with theSwift Android SDKinto a shared library. A Jetpack Compose host loads it and drives it over JNI. The command stream is read directly out of native memory, no copy, so the Compose host decodes the same bytes the browser does.
Where the browser host paints a control onto the canvas, the Compose host mounts a real Material switch, text field or date picker at that rectangle, with native focus, ripple, keyboard, and accessibility.
Both phones below are running the same 71-line SwiftUI source: the left is a booted iPhone simulator; the right is the APK on an Android emulator.
!Image 4: the app built from one SwiftUI source, captured on a real iPhone simulator
iOS
!Image 5: the same build as a per-app APK on the Android emulator, painted by the Compose host
Android
**TEXT_INPUT _From_**plain text in the grouped row _vs_ Material filled text field
SF Symbols and Material Symbols are different icon sets with different names, so one generated mapping table covers both hosts. Compose’s density is pinned to the engine’s layout scale rather than the device’s, so controls land exactly on the rectangles the engine resolved. Per-app APKs don’t rebuild the host. The shared library, app name, and icon are stamped into a prebuilt template.
iPad is the same engine with a different device descriptor. The viewport, safe areas and size classes change, and SwiftUI’s adaptive layouts do the rest.
Development loop
Apple simulator
our engine
difference
_agents on the engine's framework source_ 4 editing right now
- `Charts/ChartRender.swift`done
- `SwiftUI/Core/ShapeStyle.swift`done
- `SwiftUI/Modifiers/TextLayoutModifiers.swift`done
- `SwiftUI/Generated/LayoutParams.swift`editing
- `SwiftUI/Core/ColorGradient.swift`queued
- `Charts/ChartPlots.swift`editing
- `SwiftUI/Modifiers/ScrollModifiers.swift`done
- `SwiftUI/Core/SemanticColors.swift`editing
- `UIKit/GestureScrollControl.swift`queued
- `Charts/ChartViewModifiers.swift`editing
- `SwiftUI/Core/Animatable.swift`queued
- `CoreGraphics/CGPath.swift`done
- `…`and the rest of the red
re-render, diff again, until they agree
Development of the engine is highly automated using a fleet of agents. The agents use Apple’s SDK artifacts to build an API reference. We then render a variety of scenes in a booted iOS simulator, capture them, and diff the engine’s output against them pixel by pixel. When a new iOS ships, we re-render the source of truth against the new simulator.
Screenshots miss anything that moves, so a filmstrip runner drives a virtual clock: it ticks the engine in fixed steps, samples frames on a timeline, and fires synthetic taps and key presses at exact moments.
For games we diff states and events, like where a character actually is onscreen at frame N. Our engine and the real simulator dump the same per-frame node schema (position, rotation, scale, alpha, physics velocity), and the trajectories are diffed per node per frame.
Each diff is handed to a visual large language model that identifies issues, then fans out agents to edit the engine’s framework source until the diff is under a threshold. This is a surprisingly effective process, and it gets us very close to implementing a continuously moving framework like SwiftUI.
See Dactyl in action
That’s it for the technical details behind the scenes. We haven’t even touched on how Dactyl helps you install your apps on your device for testing, though, or how it guides you through listing apps on the App Store. But maybe it’s best if you see for yourself.
Just describe an app, and everything we’ve described here will build it, live in your browser.Give Dactyl a try now.