Skip to content

iOS simulator leg: listbox_demo driven through AetherUIDriver (#22) - #166

Merged
nicolas-maman merged 9 commits into
mainfrom
feat/22-ios-simulator-leg
Sep 18, 2026
Merged

nicolas-maman merged 9 commits into
mainfrom
feat/22-ios-simulator-leg

Conversation

@nicolas-maman

@nicolas-maman nicolas-maman commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Closes #22 — its acceptance criterion, run on every push: an aether-ui app linked against the UIKit backend and an iOS build of libaether, running on iOS, widgets rendered, taps firing their DSL closures.

The leg (ci.sh Phase 1e2, macOS runner)

  1. ae build --target=aarch64-ios-simulator --emit=staticlib tests/ios/runtime_seed.ae → the Aether runtime compiled for the simulator (aether #1385's toolchain half, #1654/#1868 upstream).
  2. aetherc examples/listbox_demo/listbox_demo.ae → its portable C, as build.sh emits it.
  3. xcrun clang -target arm64-apple-ios17.0-simulator links app + aether_ui_uikit.m + driver against that archive and the frameworks — Phase 1e's link line with the real runtime where tests/ios/link_stub.c stood.
  4. A bundle (tests/ios/Info.plist, scene manifest so the backend takes its UIWindowScene path), simctl boot / install / launch with SIMCTL_CHILD_AETHER_UI_TEST_PORT.
  5. tests/listbox_demo/spec_listbox_demo.ae runs against it unchanged — 9/9: rows rendered by UIKit, a tap selecting a row through the DSL's closure, on_select firing, programmatic select, selection reset on update, a 200-row update driver-visible, Down/Up/Home/End moving the selection.

On failure the phase prints whether the process is alive, its unified log, and the crash report's exception, exception backtrace and faulting thread with image names.

What the first runs found, and fixed here

  • The driver read UIKit's view tree off the main thread and died in -[__NSArrayM objectAtIndex:] on the first /widgets. UIKit, like GTK4, is not for reading off its thread: the adapter now supplies run_on_ui_thread, so every request is serviced on the main queue, and the rest of the table AppKit and Win32 fill (text, visibility, parent, children, rect, enabled, classes, focus, hover/pressed, screenshot, and dispatch_action for click, set_text, toggle, set_value, set_state, focus, key, split, tabs, context menu, pick, hover/press/release, the canvas events, shutdown). Window resize, menu bar and tray answer 404: iOS has none.
  • UIKit never retired what it removed. clear_children, remove_child, set_child, navstack_pop left views in the registry: alive for the life of the process, listed by the driver with stale text and parent (the spec found "person 2" in a row of a rebuild that was gone and clicked that), closure boxes never given back. aeui_unregister_view_tree retires them; boxes are released on the next main-queue turn (a closure retiring its own widget is on the stack — win32 gives back the closure boxes a retired widget owned #145's lesson). AeuiClosureHolder is the protocol every closure-holding helper adopts.
  • A UIKit container could not take focus, so a listbox's rows never owned it and arrow keys did nothing. AeuiStackView answers canBecomeFirstResponder from the set_focusable flag (AppKit's AetherStackView does the same) and delivers hardware-keyboard presses to the window's key handler.
  • picker_set_selected fires the change callback as on the other backends; on_click keeps its closure addressable by handle.

README and CHANGELOG updated; the UIKit header says what CI runs.

Phase 1e2 on the macOS leg: `ae build --target=<arch>-ios-simulator
--emit=staticlib` compiles the Aether runtime for the simulator
(tests/ios/runtime_seed.ae), aetherc emits listbox_demo's portable C, clang
links app + UIKit backend + driver against that archive and the frameworks,
the binary goes into a bundle (tests/ios/Info.plist), the bundle into a
booted iPhone simulator, and tests/listbox_demo's spec runs against the app
over the driver port, unchanged from the desktop backends.

This is the acceptance #22 set out and could not meet while the toolchain
half (aether #1385) was open: an aether-ui app linked against the UIKit
backend and an iOS build of libaether, running on iOS, widgets rendered,
taps firing their closures.
The first run got as far as a booted simulator with the bundle installed
and launched, and no driver answer in 20s, with nothing on the app's
stdout or stderr. The phase now waits 40s and, on a silent app, prints
whether the process is alive, the unified log for it, and the head of its
crash report if it left one.
The simulator leg's first run found the driver's partial table: the app
launched, the server bound, and the first /widgets request walked the view
tree from the HTTP thread and died in -[__NSArrayM objectAtIndex:]. UIKit,
like GTK4, is not for reading off its thread. The adapter now supplies
run_on_ui_thread, so the shared server services every request on the main
queue, and the rest of the table AppKit and Win32 fill: text, visibility,
parent, children, rect, enabled, classes, focus, hover/pressed readback,
screenshot, and dispatch_action for click, set_text, toggle, set_value,
set_state, focus, key, split, tabs, context menu, pick, hover/press/
release, the canvas events and shutdown. What iOS has no counterpart for
(window resize, menu bar, tray) answers 404 rather than pretending.

Two parity fixes on the way: on_click keeps its closure addressable by
handle so a driver click on a plain container (a listbox row) fires it,
and picker_set_selected fires the change callback as it does on the other
backends. The crash-report extraction in the leg's diagnostics now prints
the exception, its backtrace and the faulting thread with image names.
The simulator's first spec run passed the button clicks and failed every
row-selection case: clear_children, remove_child, set_child and
navstack_pop took views out of the tree and left them in the registry, so
the driver listed the rows of every rebuild that was gone, found "person
2" in a retired one, and clicked a row that no longer existed. The same
slots kept every retired view alive for the life of the process and never
gave their closure boxes back.

Retired views now leave the registry (aeui_unregister_view_tree, what
AppKit's unregister_view_tree and win32's mark_subtree_dead do), and the
boxes their helpers hold are released on the next turn of the main queue,
since the closure retiring its own widget is on the stack when this runs
(#145's lesson). AeuiClosureHolder is the protocol every closure-holding
helper adopts so one walk finds them all.
…reach the window handler

The simulator run now selects rows by tap and programmatically; the
keyboard cases still failed because focus_impl on a row did nothing:
UIView answers NO to canBecomeFirstResponder and has no setter, so
_listbox_owns_focus never saw a row focused and the arrow keys were
ignored. AeuiStackView answers from a flag set_focusable sets, as
AppKit's AetherStackView does, and delivers hardware-keyboard presses to
aether_ui_window_key_deliver with the desktop backends' key names.
@nicolas-maman
nicolas-maman merged commit 403d2ae into main Sep 18, 2026
3 checks passed
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.

iOS: UIKit backend — a 4th implementation of aether_ui_backend.h (AppKit as reference)

1 participant