Follow-on to #89. Five actions — openCamera, pickImage, pickFile, saveFile, pickContact — and the delegate factory, the last of the three mechanisms this migration needed after async replies and the event channel. Everything remaining is repetition of a proven pattern.
The factory owns class registration and nothing else
Routing callbacks to pending requests was tempting and would have been wrong: the location delegate serves a one-shot and a running stream at once, a picker is modal and serves exactly one call, the message handler serves none. Three lifetimes; a factory that guessed at one would push the other two into working around it.
It also does not retain the instance, and says why — UIImagePickerController and CNContactPickerViewController hold their delegate weakly, so dropping the reference is a use-after-free the first time the user taps Cancel, not a leak. And a partially-built class is never registered: an unregistered pair leaks one allocation, while a registered class missing a delegate method silently does nothing when the framework calls it.
Errors that name their own cause
The review raised something it correctly declined to decide alone: a cancelled picker was rejecting with NATIVE_CALL_FAILED. A user tapping Cancel is not a native call failing — the call worked and the answer is "no" — and reporting otherwise sends whoever reads the error looking for a bug that isn't there.
ios_async.deliverErrorCode is the missing channel. The filepicker turned out to already have the information: its Outcome enum distinguishes cancelled, no_selection, dismissed and unreadable, and only the last is a genuine failure. The distinction existed; the wire had nowhere to put it.
A latent crash
dismissPresented(picker, resolved: ?Id) took a double optional — Id is already ?*anyopaque. A .some(null) unwrapped to a null SEL sent to a live receiver: a nil receiver is a no-op, a nil selector is not. Flattened, with the collapse extracted so it's assertable on a host.
The sixteenth assertion, and three wrong guesses
It checks that a refusal names its own cause, and it took me three tries because I kept guessing the code instead of reading it. openCamera on the fixture refuses with PERMISSION_DENIED — not NOT_FOUND, not PLATFORM_NOT_SUPPORTED — because the Info.plist has no NSCameraUsageDescription, and presenting a camera picker without one terminates the process rather than failing. The module refuses before it ever asks whether hardware exists. It was right; my assertion was wrong, twice.
ok: unavailable sensor refused rather than faking a stream (i=16 absent)
ok: event-driven action dispatched to Zig (i=60 seen 1x)
ok: unavailable hardware refused with a specific code (i=18 seen 1x)
PASSSixteen assertions on an iPhone 17 Pro simulator, plus zig build test / test:ios / build-ios / build-ios-simulator / build-android, zig fmt --check, verify:cimports, pickier.