Skip to content

Add NEUI_API_IOS, an iOS-only host extension - #33

Merged
defiantnerd merged 1 commit into
mainfrom
feature/ios-host-extension
Sep 19, 2026
Merged

defiantnerd merged 1 commit into
mainfrom
feature/ios-host-extension

Conversation

@defiantnerd

@defiantnerd defiantnerd commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

UIKit exposes a lot that has no portable equivalent, and a client had no way to reach any of it: keep the screen awake through a set, stop a thumb near the bezel from swiping the app away mid-song, ask whether the device dropped into Low Power Mode, fire a haptic tick.

What this adds

NEUI_API_IOS (include/neui/d/ios.h) — a 22-method vtable returned only by neui.host.ios and the xpl host on platform_ios.mm. Every other host returns NULL from get_interface, the same feature-detect contract NEUI_API_EMBED and NEUI_API_METRICS already use. Four groups: idle timer / screen-edge gesture deferral / home indicator / brightness; status bar / orientation / forced appearance; Dynamic Type category and the accessibility switches; device / battery / thermal / Low Power / haptics / keyboard inset.

Unlike the xpl host's inert NEUI_API_EMBED on iOS, it is never handed out empty: a host that returns it implements every method, and a call it cannot answer says so in its return value. Changes arrive as one NEUI_EVENT_IOS_ENVIRONMENT_CHANGED carrying a bitmask of what moved, in its own event category — Dynamic Type stays on METRICS_CHANGED, where clients already handle it.

Safe-area insets are deliberately not here. They are portable, they already exist in NEUI_API_METRICS, and an Android host can implement the same seam from WindowInsets. Entries that do have an Android counterpart are marked inline for whoever writes that host; three are marked Android: none.

Seams are registries now, including the metrics ones

neui_init() registers the native iOS host and then xpl, whose platform_init() runs last, so an assigned single-slot seam always ended up holding xpl's — and for a native-host client it failed silently.

This was already true of NEUI_API_METRICS. safe_area_insets measured zeros on every edge in a binary linking both hosts, masked because get_client_rect computes its top inset directly rather than through the seam. Confirmed by temporarily restoring last-wins and re-running examples/ios:

old (last-wins):  safe_area_insets: l=0 t=0   r=0 b=0
fixed:            safe_area_insets: l=0 t=106 r=0 b=34

Both seam families now add rather than assign; the frame lookups try each until one claims the frame. The safe-area seam signature became bool so "not my frame" is distinguishable from "mine, and the insets are zero". The example prints its insets permanently, so a regression shows up on the next run.

Verification

result
iOS 26.5 simulator 351 cases / 2107 checks, 0 failed
macOS 343 / 2046, 0 failed
Both, -Werror clean

tests/test_metrics.cpp is new and covers the seam rule with two fake hosts — metrics.h had no test at all before. tests/test_ios_api.cpp pins the enum and bit-flag ABI on all four CI jobs; tests/test_ios_api_device.mm is iOS-only and runs on the simulator in CI.

Not verified: battery, thermal, Low Power, brightness and whether haptics actually fire need hardware — the simulator can only confirm they answer in range.

Docs

docs/host-ios.md is new and entered through the CLAUDE.md subsystem index, which is the path an agent actually follows. Also corrected what would otherwise mislead: CLAUDE.md's platform list and host table, TODO.md listing iOS as unported, the neui.ios.* namespace missing from attrs.h, and the "phase-2 stubs" claims in hosts/ios/host.h and platform_ios.mm for widgets that are long since implemented.

UIKit exposes a lot that has no portable equivalent, and a client had no way
to reach any of it: keep the screen awake through a set, stop a thumb near the
bezel from swiping the app away mid-song, ask whether the device dropped into
Low Power Mode, fire a haptic tick.

NEUI_API_IOS (include/neui/d/ios.h) is returned only by the two iOS hosts;
every other host returns NULL from get_interface, the same feature-detect
contract NEUI_API_EMBED and NEUI_API_METRICS already use. Unlike the xpl host's
inert NEUI_API_EMBED on iOS, it is never handed out empty: a host that returns
it implements every method, and a call it cannot answer says so in its return
value. Changes arrive as one NEUI_EVENT_IOS_ENVIRONMENT_CHANGED carrying a
bitmask of what moved, in its own event category.

Safe-area insets are deliberately NOT here. They are portable, they already
exist in NEUI_API_METRICS, and an Android host can implement the same seam from
WindowInsets. Entries that do have an Android counterpart are marked inline for
whoever writes that host; three are marked "Android: none".

Implementation is shared between the hosts (hosts/shared/ios/ios_api.h) because
UIKit's globals are identical in both. They differ in two things only, and both
are seams: resolving a frame to its UIViewController, and walking the host's
own registry to deliver an event.

Those seams are registries rather than single slots, and so are the metrics
ones now. neui_init() registers the native iOS host and then xpl, so an
assigned slot always ended up holding xpl's - and for a native-host client it
failed silently. This was already true of NEUI_API_METRICS: safe_area_insets
measured zeros on every edge in examples/ios, masked because get_client_rect
computes its top inset directly rather than through the seam. Both now ADD,
the frame lookups try each until one claims the frame, and the example prints
its insets so a regression shows up on the next run.

Verified on an iOS 26.5 simulator (351 cases / 2107 checks) and on macOS
(343 / 2046), both -Werror clean. Battery, thermal, Low Power, brightness and
whether haptics actually fire still need hardware.

Docs: docs/host-ios.md is new and entered through the CLAUDE.md subsystem
index, which is the path an agent follows. Also corrected what would otherwise
mislead - CLAUDE.md's platform list and host table, TODO.md listing iOS as
unported, the neui.ios.* namespace missing from attrs.h, and the "phase-2
stubs" claims in hosts/ios/host.h and platform_ios.mm for widgets that are
long since implemented.
@defiantnerd
defiantnerd force-pushed the feature/ios-host-extension branch from 1f2885e to 480891b Compare September 19, 2026 18:41
@defiantnerd
defiantnerd merged commit 331129e into main Sep 19, 2026
8 checks passed
@defiantnerd
defiantnerd deleted the feature/ios-host-extension branch September 19, 2026 19:30
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.

1 participant