All notable changes to mob are documented here.
Format: Keep a Changelog. Versioning: SemVer.
Full module documentation: hexdocs.pm/mob.
[0.7.0] - 2026-06-12 — the plugin-extraction major (BREAKING)
Added
- Pure-Elixir composite components (
Mob.Composite): UI kits register tag-name expanders (the manifestui_componentsexpand:form, orMob.Composite.register/2) and<MyTag …/>expands to built-in widget trees in a new FIRST render pass — fixpoint with a depth guard, crash-isolated.on_*props written as bare strings/atoms are auto-injected as{screen_pid, tag}(no more threadingself()). Hot-pushable. Seedecisions/2026-06-11-composite-expansion-pass.md. - Route-bound navigation params (
Mob.Nav.Registry.register/3+lookup_route/1): a registered route can carry a params map merged under push params intomount/3— the enabler for data-driven plugins (mob_ash registers/ash/postas{MobAsh.ListScreen, %{resource: …}}). Screen-manifest entries take an optional:params. - Style packages, tokens-only tier (MOB_STYLES.md implemented in part): the runtime manifest carries
styles/default_style; boot applies the default style's theme (Mob.Plugins.apply_default_style/0). The five preset themes ship in themob_themespackage. - Boot-time plugin NIF loading (
mob_notify_set_screen_pidseam,host_requirementsprinting,compositesboot registration) — the plugin-system core wiring landed across this cycle; see MOB_PLUGINS.md.
Removed (BREAKING — each capability moves to its plugin package)
Mob.Camera→mob_camera(thecamera_previewnode stays in core)Mob.Location→mob_locationMob.Notify→mob_notify(delivery plumbing — delegate, push-token forward, launch handoff — stays in core; pairs with the server-sidemob_push)Mob.Photos→mob_photosMob.Biometric→mob_biometricMob.Scanner→mob_scanner(requiresmob_camerafor the:camerapermission)Mob.Bt→mob_bluetooth(Wave 1)- Themes
Obsidian/ObsidianGlass/Citrus/Birch/Material3→mob_themes(light/dark/adaptive remain the neutral baseline) No deprecation shims (see plugin_extraction_plan.md for the policy rationale). Migration: add the package dep + activate inmob.exs; module names change (Mob.Camera→MobCamera,Mob.Theme.Citrus→MobThemes.Citrus, …).
[0.6.26]
Added
- Plugin documentation, shipped with the package. A "Writing a Plugin" authoring guide (
guides/plugins.md: scaffold → implement → sign → activate → deploy, per tier, with a worked-examples index) plus the manifest reference (MOB_PLUGINS.md) and security/trust doc (MOB_PLUGIN_SECURITY.md) are wired into ex_doc/HexDocs (a Plugins extras group + aMob.Pluginsmodule group). The reference now documents cross-plugin conflict detection (every guarded shared resource + the completeness guarantee) and the runtime plugin manifest + its build-time auto-regen. Mob.Pluginsruntime hardening. Notification dispatch is crash-isolated — a handler or predicate that raises is logged and skipped instead of taking down the host screen GenServer (mirrors the lifecycle dispatcher). A malformed settings schema (missing:default/:type) logs + falls back instead of crashing reads/writes, andregister_screensrejects anilmodule/blank route at registration rather than deferring the error to navigation.- Custom fonts (app-level + plugin). mob's
font:prop (documented but only half-built) now works end-to-end:mix mob.deploy --nativebundlespriv/fonts/*.ttf|otfand pluginassets.fontsinto the platform bundle — iOS into the.app+Info.plistUIAppFonts(feeding SwiftUIFont.custom), Android intores/font/<normalized>(uncompressed; the renderer loads it by resource id, fixing the previousTypeface.createstub that only handled system families). Visually confirmed on Android: a plugin-shipped font renders distinct from the system font. - Plugin tiers 3 (multi-screen) and 4 (embedded sub-app). See
decisions/2026-06-06-plugin-tiers-3-4.md. Both are pure-Elixir and runtime-wired off a generated runtime manifest (priv/generated/mob_plugins.exs, written bymix mob.regen_plugin_manifest) that the newMob.Pluginsmodule reads at boot. Tier 3: plugins ship wholeMob.Screenmodules (static:screensor spec-v2:screens_generatorcodegen run under the host-config audit), registered as navigable routes inMob.Nav.Registry; plus:migrations(build-copied into the host migrations dir, namespaced + version-preserving, run by the host'sEcto.Migrator) and:assets. Tier 4::lifecycle(on_start+ supervised children +on_resume/on_backgroundviaMob.Plugins.Supervisor/LifecycleandMob.Device),:settings(Mob.Plugins.get_setting/2/put_setting/3onMob.State, schema-validated, with aneditor_screen), and:notifications(Mob.Plugins.dispatch_notification/1first-match routing). Device-verified on a physical iPhone (SE) and Android (Moto G): static + generated screens register, a plugin migration creates its table on device, and tier-4 on_start / supervised worker / settings / notification routing all work.Mob.Plugins.bootcaptures the host OTP app name at compile time viause Mob.App(a mob release boots withoutApplication.start, soApplication.get_application/1is nil at runtime).
Changed
- Location fully extracted to the standalone
mob_locationplugin (Wave 2). Seeplugin_extraction_plan.mdanddecisions/2026-06-05-mob-location-extraction.md.Mob.Location(get_once/start/stop), the iOSCLLocationManagerNIFs + delegates, the AndroidFusedLocationProviderClientZig NIF +mob_deliver_location, and the hardcoded"location"branch ofnif_request_permissionare removed from core (lib/mob/location.ex,ios/mob_nif.m,android/jni/mob_nif.zig,src/mob_nif.erl).mob_locationis a cross-platform tier-1 plugin: it ships an Objective-C iOS NIF (lang: :objc) and an Android Zig NIF (lang: :zig, viaMobLocationBridge), registers the:locationcapability through the extensible permission registry (iOSmob_register_permission_handler, AndroidMobPermissionProvider), and declares its Android permissions + iOS plist key +play-services-location+CoreLocationframework in its manifest (mob_dev merges these into the host at build time). Breaking: core no longer provides any location surface and there is intentionally no compatibility shim. Apps that usedMob.Location.*should add{:mob_location, "~> 0.1"}(orpath:/github:) and callMobLocation.*. The same location surface was removed from themob_newgenerated-app templates. Device-verified on a physical iPhone (SE) and Android (Moto G) both before and after the core strip —MobLocationround-trips real fixes through the plugin alone, and:mob_nif.location_get_once/0now raisesUndefinedFunctionError.
Fixed
- iOS: stop capping the literal super-carrier at 10 MB.
mob_beam.mappended a hardcoded-MIscs 10after the configured flags; since allocator flags are last-wins, it silently overrode the 0.6.24-MIscs 128default (and anymob_beam_flagsoverride), so the literal area was always 10 MB. A large app (e.g. embedded Livebook) plus a notebook'sMix.installfilled it and the VM aborted withliteral_alloc: Cannot allocate .... Removed the hardcoded cap; the-MIscs 128default now takes effect (iOS accepts a 128 MB reservation). Verified on a physical iPhone:emu_argsshows a single-MIscs 128andMix.installreturns:ok.
[0.6.25]
Added
- "Open with" — receive a file another app opens into yours. New
Mob.Files.take_opened_document/0returns%{path, name, mime, size}(or:none) for a file handed to the app (e.g. a notebook emailed and tapped), parallel toMob.Files.pick/2's{:files, :picked, …}. Call it from your root screen'smount/3; a file opened while already running arrives as{:files, :opened, item}(iOS). New NIFtake_opened_documentplus C-exportmob_set_opened_documenton both platforms (iOSapplication:openURL:options:→mob_handle_opened_url; AndroidMainActivityreads the ACTION_VIEW/SEND intent →MobBridge.setOpenedDocument). The app declares the document type (iOSCFBundleDocumentTypes, Android<intent-filter>) and forwards the open. Verified end-to-end: a.livemdopened into the embedded-Livebook app opens as a notebook on a physical iPhone and a physical Android (Moto G).
[0.6.24]
Fixed
- iOS: enlarge the BEAM literal super-carrier to 128 MB (
-MIscs 128default flag). iOS can't reserve the OTP default 1 GB literal virtual area and falls back to ~10 MB. A large app such as an embedded Livebook plus a notebook'sMix.installfills that 10 MB and the VM aborts withliteral_alloc: Cannot allocate N bytes (of type "literal"). The iOS native launcher's default flags now request a 128 MB literal carrier — a virtualMAP_NORESERVEreservation (commits physical only on use) that iOS accepts where 1 GB fails. Apps no longer need a per-appbeam_flags:override for this. iOS-only; Android keeps its normal large carrier. A runtimemob_beam_flagsoverride still wins. Verified on a physical iPhone: embedded Livebook serves andMix.install([{:short_uuid, "~> 0.1"}])returns:ok.
[0.6.23]
Added
- Element positions without a screenshot.
element_frames/0NIF surfaced asMob.Test.element_frames/1(%{id => {x,y,w,h}}),frame/2, andtap_id/2(drive by id at real coordinates). Any rendered node given an:idreports its live on-screen frame (logical points iOS / dp Android) to a registry the agent reads over dist — a compact structured map instead of image bytes, with no accessibility activation. The renderer also sets the:idas the element's accessibility identifier (iOSaccessibilityIdentifier, Android ComposetestTag), so the same tags are visible to XCUITest/Espresso. Opt-in per element: untagged nodes cost nothing (the tracking modifier only attaches when an:idis present). iOS records the full element frame via aGeometryReaderbackground; Android viaModifier.onGloballyPositioned. Verified on iOS sim, Android device, and a physical iPhone. The Android Kotlin side lives in themob_newMobBridge.kt.eextemplate. - In-process screenshot + scroll control over dist (no adb/xcrun). Three test-harness NIFs (
screenshot/3,scroll_info/1,scroll_to/3) surfaced asMob.Test.screenshot/2,scroll_info/2,scroll_to/4, andscreenshot_tour/3. A remotely-connected agent gets pixels and deterministic scroll entirely over Erlang distribution — the capability Sloppy Joe and WireTap need to drive a device an agent can only reach over dist. Capture is in-process (iOSUIGraphicsImageRenderer+drawViewHierarchy; AndroidPixelCopyagainst the activity window). Scroll views are addressed by their:idprop;scroll_inforeportskind: :pixel(iOSUIScrollView, AndroidverticalScroll) or:index(AndroidLazyColumn, where y is an item index and viewport is the visible-item count). Captures the app's own surface only —FLAG_SECURE/secure fields render blank, and a backgrounded app returns{:error, :no_window}. The Android Kotlin side (screenshot/scrollInfo/scrollTo) lives in themob_newMobBridge.kt.eextemplate; existing apps pick it up on regeneration. Debug-only (iOS#if !MOB_RELEASE). Seedecisions/2026-05-29-bridge-nif-screenshot-scroll.md.
Changed
Mob.Btfully extracted to the standalonemob_bluetoothplugin (Wave 1 complete). Seeplugin_extraction_plan.md. Session A moved the Elixir wrappers (Mob.Bt,Mob.Bt.Hfp,Mob.Bt.Hid,Mob.Bt.Spp) out of core; Session B now removes the native side too — the Bluetooth Zig NIF fromandroid/jni/mob_nif.zigand the iOS unsupported-stubs fromios/mob_nif.m.mob_bluetoothis now a tier-1 plugin: it ships its own Zig NIF, JNI thunks, andMobBluetoothBridgeKotlin, and declares its Android permissions + iOS plist keys in its manifest (mob_dev merges these into the host app at build time). Breaking: core no longer provides any Bluetooth surface and there is intentionally no compatibility shim. Apps that usedMob.Bt.*should add{:mob_bluetooth, "~> 0.1"}(orpath:/github:) and rename references toMobBluetooth.*. HID input and SCO PCM streaming were never implemented and are not part of the plugin (HID is platform-blocked on Android; see the plugin's docs).
[0.6.22]
Added
Mob.Certs— load CA certificates from a PEM bundle into Erlang's:public_keycacert store. Android's system trust store lives behind a Java API that:public_key.cacerts_load/0(no-arg) can't reach, so the first TLS call from Req / Mint / Finch crashes withno_cacerts_found(orFunctionClauseErrorin some OTP versions). Apps bundle a PEM (conventional source: copycastore'scacerts.pemintopriv/at build time) and callMob.Certs.load_cacerts!(Application.app_dir(:my_app, "priv/cacerts.pem"))once at boot. iOS and the Android emulator aren't affected; calling unconditionally is harmless there. Verified end-to-end on a Moto G Power 5G 2024 (Android 14):Mix.install([{:req, "~> 0.5"}])thenReq.get!("https://geocoding-api.open-meteo.com/v1/search?name=Vancouver")returns200.mob_beam.zigexportsMOB_NATIVE_LIB_DIRbefore BEAM start — the absolute path of the app's nativeLibraryDir, which the APK install hash makes unpredictable at compile time. Apps that bundle runtime binaries (escript, rebar3, etc.) aslib*.soneed this to setMIX_REBAR3and locate the bundled escripts.- Optional ERTS-extras symlinks (
escript/erlexec/erl/beam.smp) inmob_beam.zig. Silent-skips when the lib isn't in nativeLibDir, so non-opting-in apps see no behaviour change. Apps that droplib<name>.sointoandroid/app/src/main/jniLibs/<abi>/get a workingBINDIR/<name>— enough for runtimeMix.installof rebar3-built deps (telemetry, jose, jiffy, …) to bootstrap a fresh VM.erlanderlexecboth target the sameliberlexec.sobecause they are the same binary (erlexec doesn't switch onargv[0]).
Changed
extra_applications: [:logger, :public_key]— Elixir 1.19+ strips unused OTP applications from the code path;Mob.Certscalls:public_key.cacerts_load/1at runtime, so its.beammust be in the path even though mob doesn't start:public_keyitself.
Fixed
mix.exs— collapsed duplicatebefore_closing_body_tag/1clauses introduced in 0.6.20. The mermaid clause's_catchall shadowed an older language-elixir highlighter clause, leaving it as dead code (and emitting compile warnings). The unified clause emits both scripts; the duplicatedocs/0keyword entry was removed.
Docs
common_fixes.md— new section documenting the Android cacerts symptom (no_cacerts_found/FunctionClauseError) and the load-PEM-at-boot fix; also the bundled-OTP-extras pattern (wrapper script, rebar3 module-name derivation,$ROOTDIR/bin/*.bootmaterialization) for apps that opt into runtime rebar3.
[0.6.21]
Added
Mob.DNS.resolve/1now works on Android.nif_resolve_ipv4(android/jni/mob_nif.zig) calls Bionic'sgetaddrinfoin-process and seeds:inet_db's:filetable, mirroring the iOS NIF added in #32. Physical Android devices return:nxdomainfrom BEAM's default DNS path (forkinginet_gethostas a port program) even when the same app's in-process HTTPS stack resolves the hostname fine — the emulator masks this. Verified end-to-end on a Moto G Power 5G 2024 (Android 14):Mob.DNS.resolve("repo.hex.pm")returns the right IP,:inet.getaddr/2then succeeds via the seeded entry, andMix.install([{:dep, "~> ..."}])from a notebook setup cell resolves, fetches, and compiles on-device. Bionicaddrinfo/sockaddr_in/getaddrinfo/freeaddrinfo/EAI_*bindings added toandroid/jni/mob_zig.zig. Suspected root cause islibnetd_client.so's netd routing not surviving execve; the NIF sidesteps it by running in the app's own process.
Changed
Mob.DNSmoduledoc — dropped the "Android isn't affected" claim. Added a background-app caveat: Android App Standby blocks all outbound network from a backgrounded mob app (TCP-by-IP, not just DNS — surfaces as:closed/:timeouton any socket attempt). Fix is a foreground service or keep the app foregrounded; not a mob bug.
Docs
common_fixes.md— new section documenting the:nxdomainsymptom on physical Android, the foreground-app caveat, and the fix.
[0.6.18]
Changed
RUSTLER_NIF_LIB_PATH→RUSTLER_BEAM_LIBRARY_PATHinmob_beam.zig's host setenv block. Matches the env var name filmor chose for the alternative upstream rustler PR (rusterlium/rustler#733), which is what'll land upstream instead of our #726. End-to-end tested on physical arm64 Android with filmor's branch: Mob sets the env var → rustler reads it → Rust NIF resolves and executes. Mob users on rustler 0.37 Hex release (no patch) see no change; users on the GenericJam fork OR on whatever rustler version eventually ships #733 get matching behaviour.
[0.6.17]
Added
Mob.Audio.play_at/4— sample-accurate scheduled audio playback. Takes an absolute local wall-clock target (System.system_time(:millisecond)ms-since-epoch) and hands it to the audio hardware clock for firing, rather than waking the BEAM viaProcess.send_after. The hardware-clock path eliminates timer-wheel + scheduler jitter from the end-to-end sync error, leaving per-device first-sample latency (~30–80 ms, calibratable) as the dominant remaining term. iOS only in this release; Android still falls through to the existingMediaPlayerpath (port to AAudio is pending).- iOS:
nif_audio_play_at(Path, OptsJson, AtWallMs)backed by a dedicatedAVAudioEngine+AVAudioPlayerNode. The wall-time target is converted to anAVAudioTimehostTimeviamach_absolute_time+mach_timebase_info, then handed to-[AVAudioPlayerNode scheduleBuffer:atTime:options:completionHandler:]. Past targets schedule ASAP. Multipleplay_atcalls accumulate on the player's timeline — useaudio_stop_playbackto flush. audio_set_volumeandaudio_stop_playbacknow also reach the scheduled-engine player so cross-API mixing behaves sanely.
Use case
- Distributed orchestra / multi-device musical performance where every phone must start the same sample at the same wall-clock instant. Pair with an NTP-style server-clock-sync helper on the caller side; this API takes the converted local-clock target.
[0.6.16]
Added
mob_beam.zigexportsRUSTLER_NIF_LIB_PATHbefore BEAM start. Callsdladdr(&mob_start_beam)to discover the absolute path of the host.so(e.g.lib<app>.so) andsetenv()s it asRUSTLER_NIF_LIB_PATH. Pairs with the matching upstream rustler change (rusterlium/rustler#726): rustler'sDlsymNifFiller::new()on Android reads the env var first, falls back to its existing dladdr-self probe when unset. End result: rustler-based Rust NIFs statically linked into Mob's main.sonow resolveenif_*symbols correctly on Bionic without any per-app patching. Existing rustler users on Android who don't run inside Mob see no change — the dladdr fallback covers them.mob_zig.zigexposesdladdr+DlInfoto other Zig consumers underjni.dladdr/jni.DlInfo. Hand-declared to match the libc/Bionic surface; same hand-declared FFI policy as the rest ofmob_zig.zig(we don't use@cImporthere).
Notes
- The setenv runs unconditionally — even apps that don't ship a rustler NIF get the env var set. Harmless. The env var only affects rustler's own startup logic when a rustler-built NIF loads.
Verified end-to-end on a physical arm64 Android device (moto g power 2021): host sets path → rustler reads env var →
dlopen(path, RTLD_NOW | RTLD_NOLOAD)→dlsymallenif_*exports → Rust NIFgreet/0executes and returns"Hello from Rust!"to BEAM.
[0.6.15]
Added
text_fieldnow accepts asecure: trueprop. iOS renders the field as a SwiftUISecureField(masked input) instead of the plainTextField. The prop flows through the existing renderer passthrough; cleartext still reaches the BEAM viaon_changeso apps can hash/store the value as normal. Android consumes the same prop viaPasswordVisualTransformationoncemob_new'sMobBridge.kt.eextemplate is updated in a companion PR — until then the prop is a graceful no-op on Android (renders as a regular field), no breakage.Reveal-toggle ("eye" button) is intentionally deferred — its interaction with SwiftUI focus retention requires a
ZStack-and-opacity rebuild ofMobTextFieldand warrants its own change.
Fixed
- iOS:
Mob.App.start/0now switches:inet_dbto file-only lookup and seedslocalhostbefore any user code runs — BEAM's default:nativelookup tries toexecvetheinet_gethostport program, which the iOS sandbox refuses, crashing the firstNode.connect/:erpc.call/gen_tcp.connect/3with:badarg. Apps no longer need to set the lookup chain themselves;Mob.DNS.configure_pure_beam/1still composes on top for outbound DNS. Seeguides/dns_on_ios.md. - iOS:
Columnnow honoursfill_height: true. The.columncase inMobRootViewonly setmaxWidth, so aColumnwithfill_height: truewould collapse to its children's natural height — breaking the canonical<Column fill_width fill_height>header/flex/footer pattern. Now setsmaxHeight: .infinitywhen the prop is set and switches alignment to.topLeadingso children anchor at the top when the column flexes. Default (nofill_height) behavior is unchanged.
Docs
- Plugin system design corpus:
MOB_PLUGINS.md(capability-plugin manifest, tiers 0-4, spec-v2 code-generated plugins),MOB_STYLES.md(style preset system, namespaced cherry-pick, stable per-primitive prop contract),MOB_PLUGIN_SECURITY.md(three-layer trust model, dev-mode escape hatches,:acknowledge_unsafe_plugins),plugin_extraction_plan.md(Phase 0 → Phase 3 + risk register + kickoff checklist). Locks scope to Elixir-first, BEAM-native, Gen-AI-enabled; parks full-language non-BEAM frontends at speculativeplugin_spec_version: 3. Companionagent_briefs/rustler_env_var_test.mdcovers filmor's env-var-based fix inrusterlium/rustler#726.
[0.6.14]
Added
:mob_nif.set_theme/1— push resolved theme palette to native. Lets a ComposeMaterialThemewrapper follow runtimeMob.Theme.set(...)calls instead of being baked into MainActivity at compile time. Otherwise Material 3 system chrome (NavigationBar, Button, etc.) stays at the default light scheme while the BEAM-side primitives switch to whatever theme is active — a visible mismatch when an app uses Obsidian / ObsidianGlass.Mob.Theme.resolved_palette/1— exposes the "semantic token → theme map → palette → ARGB int" resolution path that the renderer uses internally. The native side gets concrete integers it can hand toColor(...)directly.
Notes
- iOS implements the NIF as a no-op for symmetry — SwiftUI in
MobRootView.swiftrenders every surface via mob primitives with explicit color props, so there's no system chrome that needs the push. - The Android
MobBridge.setTheme(String)Java hook is looked up viacacheOptional, so older templates that predate this load fine; the NIF just returns:okwithout dispatching when the method isn't on the bridge. - The mob_new generator templates that wire
MaterialTheme↔setThemein newly-generated apps will follow in a separate release; existing apps adopt manually (aMutableStatein MobBridge.kt +MaterialTheme(colorScheme = …)wrap in MainActivity.kt).
[0.6.13]
Changed
- Liquid Glass uses
Glass.clearinstead ofGlass.regular. On dark surfaces with little behind a card to refract,.regularreads as a frosted plate rather than glass..clearis the right variant for the floating-card look the theme is meant to evoke — what's beneath shows through, the card looks like it's hovering. Only affects iOS 26+ (the.ultraThinMaterialfallback for older iOS is unchanged).
[0.6.12]
Added
Mob.Theme—glassflag for translucent surfaces. Newglass: falsefield on the theme struct. When set,Mob.Renderertags everyBoxnode that has abackground:withglass: true, and the iOS side swaps the solid fill for.glassEffect(.regular, in: shape)on iOS 26+ (real Liquid Glass) or.ultraThinMaterialon iOS 17–25 (closest fallback that ships in older SDKs). Other nodes pass through untouched. Opt in via a preset or by passingglass: truetoMob.Theme.build/1.Mob.Theme.ObsidianGlass— Obsidian palette +glass: truefor the common "make the whole app glassy" case. Switch at runtime withMob.Theme.set(Mob.Theme.ObsidianGlass); revert withMob.Theme.set(Mob.Theme.Obsidian).Mob.Theme.flags_map/1— companion tocolor_map/1/spacing_map/1/radius_map/1. Returns%{glass: bool}for now; future flag-style toggles will land here.
Notes
- Android receives the flag but ignores it for now — Compose Material 3 doesn't ship a first-class glassy surface yet; boxes fall back to solid. Compose-side support is a follow-up.
[0.6.11]
Fixed
~MOBsigil no longer double-encodes non-ASCII bytes in template source. The NimbleParsec parser usedascii_string/2for string attribute values (text="...") and brace content (text={...}); itsinteger-typed body re-encoded each source byte ≥128 as a Latin-1 codepoint then UTF-8. Net effect:–(E2 80 93) emerged asÂ+pad+O(C3 A2 C2 80 C2 93) — mojibake on screen. Swapped both call sites toutf8_string/2, which matches by codepoint and round-trips multi-byte sequences (em-dash, en-dash, middle dot, smart quotes, accents, emoji) byte-for-byte. Workaround that's now unnecessary: binding the non-ASCII string to a variable outside the sigil and referencing it viatext={var}.
[0.6.10]
Added
- iOS BEAM startup honours
MOB_NODE_SUFFIXenv var. The simulator branch already auto-derived a unique node-name suffix fromSIMULATOR_UDIDso concurrent sims didn't collide in Mac's EPMD, but there was no manual override path — the Android-sideMOB_NODE_SUFFIXconvention was iOS-blind. Now both branches (simulator + physical device) readMOB_NODE_SUFFIXwith priority: explicit env → SIMULATORUDID-derived (sim only) → none. Pairs withmob_dev 0.5.10'smix mob.deploy --node-suffix Xflag (forwarded to simctl via the `SIMCTL_CHILD*` mechanism). - Resolves the
Protocol 'inet_tcp': register/listen error: no_reg_reply_from_epmdsymptom seen when running multiple iOS sims of the same app concurrently for visual-comparison work (e.g. cross-platform theme parity).
[0.6.9]
Fixed
- CI pipeline unblocked. The 0.6.8 push failed two CI gates and never
reached Hex; this release ships the same code with the gates green:
android/jni/mob_beam.hreformatted to satisfyxcrun clang-format --dry-run -Werror(the camera-frame delivery declaration was split across three lines in a style clang-format wanted on two).decimalbumped 2.4.0 → 3.1.0 (transitive viaecto_sqlite3/jason) to clear advisory GHSA-rhv4-8758-jx7v — unbounded exponent inDecimal.new/1enables an unauthenticated DoS, affects< 3.0.0.jasonbumped 1.4.4 → 1.4.5 since older Jason cappeddecimalto~> 1.0 or ~> 2.0.
No source-level changes since 0.6.8 — same Mob.Camera.start_frame_stream/2
Android implementation and Mob.Canvas viewport docs, now actually on Hex.
[0.6.8]
Added
Mob.Camera.start_frame_stream/2now works on Android. The Camera2 + CameraXImageAnalysisuse case is wired through to BEAM as{:camera, :frame, %{bytes, width, height, format, timestamp_ms, dropped}}messages. Previously this NIF returned:unsupportedon Android — iOS-only. The Android implementation supports the sameformat: :rgb_f32the iOS side does (:bgra_u8planned for a follow-up).Mob.Canvasmoduledoc documents the viewport-scaling contract: thewidth/heightprops are logical viewport units, NOT pixels. The renderer scales draw-op coordinates against the actual on-screen pixel size. New tests intest/mob/canvas_test.exspin the contract so future readers don't regress to interpreting them as raw pixels.
Notes
- Combined with
mob_dev 0.5.9'smix mob.enable tfliteand thenx_tflite_mob 0.0.3Hex package, the cross-platform live YOLO demo (mob_yolo_demo) now runs end-to-end with only Hex deps. Measured perf: 24 ms iPhone SE A15 via Core ML → ANE; 75–117 ms Moto G Power 5G (Dimensity / BXM-8-256) via NNAPI /mtk-gpu_shim.
[0.6.7]
Added
guides/mobile_surface_matrix.md— comprehensive audit of mob's mobile capability surface vs. React Native + Expo SDK reference. Tables across UI components, gestures/input, device/system, storage, camera/audio, connectivity, sensors, location, notifications, background tasks, auth/payment, ML/Vision, maps, accessibility, iOS-only, Android-only, plus an "architecturally not present" section. Per-row status (✅ / 🟡 / ❌ / ⛔) with iOS + Android indicators. Hand-maintained from inspection oflib/mob/andsrc/mob_nif.erl. Sets realistic expectations and surfaces plugin candidates.- README link + hexdocs entry so the matrix is discoverable for new users.
RELEASE.md"Tests + docs for new functionality" section now includes amix docspreview step and clarifies that hexdocs publishing is automatic viamix hex.publish(rides along from the previously-unreleased doc improvement).MOB_PLUGINS.md— plugin manifest schema spec covering five plugin tiers (pure Elixir helper through embedded sub-app), worked examples per tier, install + activation flow, schema reference, validation rules, hot-push compatibility table, plugin_spec_version forward-compat. References from the matrix's ❌ rows as plugin candidates.
[0.6.6]
Added
RELEASE.md— canonical release-process documentation covering the mix.exs-driven trigger model, the patch-bump-default-with-mandatory- permission rule, CHANGELOG conventions, when a bump is warranted (new functionality, bug fixes, doc improvements, dep bumps) vs. when it isn't (CI tweaks, hook changes, internal refactors), the tests-and-docs-with-new-functionality non-negotiables, and the per-step idempotency ofrelease.yml. Linked frommob_devandmob_newCLAUDE.md by URL so the canonical process is one file..githooks/pre-push— committed pre-push hook that runs the cheap preflight (format + credo + warnings-as-errors) on every push and the full release preflight (test suite +mob.security_scanwhere present) only whenmix.exschanged. Activate per-clone withgit config core.hooksPath .githooks.CLAUDE.md"Release flow" section linking to the new docs.
[0.6.5]
Fixed
- HexDocs source links pointed at the non-existent
mainbranch — corrected tomasterso each</>glyph next to a heading now opens the actual source file in the GitHub repo. mob_nif.zigcalled the variadicenif_make_list/2(not exposed inmob_erts.zig) from the BT paired-list finisher; the Android arm64 build failed at link. Switched to the non-variadicenif_make_list_from_array(env, &empty, 0).
Added
.github/workflows/test.yml— runsmix test,mix format --check-formatted,mix credo --strict,mix erlfmt --check src/,xcrun clang-format,swiftlint, andmix deps.auditon push to master and on every PR..github/workflows/release.yml— on tag push, creates a GitHub Release whose body is the matching## [X.Y.Z]section from this changelog (falls back to auto-generated commit notes if the tag has no section).PLAN.md— three-layer CI + integration-test plan covering the gap between unit tests and on-device verification.
[0.6.4]
Added
Mob.GpuView/Mob.UI.gpu_view/1— Metal fragment-shader surface on iOS. Host owns the vertex shader (full-screen quad withv_uv); user supplies an MSL fragment shader plus a list of uniforms packed at natural alignment into fragment-buffer slot 0. SwiftUIMobGpuViewwraps anMTKViewwith a hash-keyed shader cache and a translucent red overlay for compile errors. iOS-only in this release; the Android GLES 3.0 backend ships in mob_new 0.3.1.<GpuView>tag whitelisted for bothpriv/tags/ios.txtandpriv/tags/android.txt.
[0.6.3]
Fixed
- iOS camera sensor delivered frames in landscape-right by default —
Mob.Camera.start_frame_stream/2was feeding 90°-rotated pixels to ML models, dropping classification accuracy enough that a jar appeared as "laptop 24%" instead of "cup 96%".AVCaptureConnection.videoRotationAngle = 90(iOS 17+) /videoOrientation = .portrait(older) is now set on both the preview layer and the data-output connection, so what the user sees and what the model sees are the same upright frame.
[0.6.2]
Added
Mob.Camera.start_frame_stream/2andstop_frame_stream/1— push-driven per-frame delivery as{:camera, :frame, %{bytes, width, height, format, timestamp_ms, dropped}}. Defaults to 640×640rgb_f32for direct Nx hand-off; caller-overridable width/height/format/facing and a softwarethrottle_msgate.
Changed
- iOS camera now uses a single shared
AVCaptureSessionfor preview and frame stream. The previous two-session design silently dropped frames because iOS allows only one active session per physical camera.
[0.6.1] and earlier
Earlier releases predate this changelog; consult the tag list and the per-tag commit messages for history.