Conversation
The shared TypeScript already ran identically on both platforms; the drift was all in the native layer. Preprocessing. Android reduces toward the target in repeated 2x steps (progressiveResize, pinned by the E13-4 golden on-device parity test). iOS resized in one shot through UIGraphicsImageRenderer's default format, whose scale is the screen scale, so a 440x440 request allocated a 1320x1320 buffer on a 3x device and 880x880 on a 2x one -- which extractNchw then resampled again. The tensor depended on which iPhone computed it: measured up to 49/255 on a channel between a 2x and a 3x device for a small crop. progressiveResize now mirrors the Kotlin algorithm and draws into an explicit pixel-sized CGContext. This buys a shared algorithm, not byte parity -- Android resamples bilinearly via createScaledBitmap, Core Graphics at .high -- and no MiewID candidate-ordering impact was measured. Numbers in kb/wildlife-reid-mobile/outputs/reports/2026-09-13-ios-resize-parity-measurement.md. Both bridge methods now decode through one loadUprightImage helper, mirroring Android's loadBitmap, so tensor and crop cannot disagree about EXIF orientation. Identity. MARKETING_VERSION was 0.0.58 while Android shipped 0.1.0-field.6; the field.4/5/6 bumps each touched only build.gradle. Version and build number now match Android. Debug takes the .dev bundle suffix Android already uses, so a dev build no longer replaces a tester's field build. The launch screen no longer reads "Off Grid", and CFBundleName is EleBook rather than the Xcode target name. Permissions. Info.plist asked for the microphone "using Whisper" and for speech recognition, and described the photo library as being for conversations. No audio dependency remains and the picker is photo-only, so both stale requests are gone and the photo string describes the app. Hook. The pre-commit SwiftLint step passed explicit paths, which overrides .swiftlint.yml's `excluded` list, so it failed on 8 pre-existing violations in ios/OffgridMobileTests that no merge gate enforces. It never fired before because the recent iOS work was done on hosts without SwiftLint installed. --force-exclude makes the hook agree with `npm run lint:ios`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the iOS target back in line with Android. The screens, stores, and services are shared TypeScript and were already identical on both platforms, so nothing here is a ported screen — everything that had drifted sits in the native layer and in app identity.
Preprocessing
ImageTensorModule.ktreduces toward the target in repeated 2x steps so a large downscale is antialiased rather than aliased — the chain pinned by Project Ganesha's golden on-device parity test (E13-4). iOS resized in one shot, and did it throughUIGraphicsImageRenderer's default format, whose scale is the screen scale. So a 440x440 request allocated a 1320x1320 buffer on a 3x device and 880x880 on a 2x device, whichextractNchwthen resampled a second time.The consequence is worse than "iOS differs from Android": the tensor depended on which iPhone computed it. Measured on an iPhone 17 Pro simulator against identical fixtures, the same photo through the old path differed between a 2x and a 3x device by up to 49/255 on a channel for a small crop.
progressiveResizenow mirrors the Kotlin algorithm and draws into an explicit pixel-sizedCGContext, so output is a function of the image alone.Full before/after numbers across four source sizes are in
kb/wildlife-reid-mobile/outputs/reports/2026-09-13-ios-resize-parity-measurement.md.Two things this does not claim. It is not byte parity with Android — the chain structure matches but the per-step filter does not, since Android resamples bilinearly through
createScaledBitmapwhile Core Graphics resamples at.high. And no MiewID impact was measured: differences of a few units out of 255 may or may not reorder candidates, and settling that needs an iOS counterpart to the E13-4 golden test, which does not exist yet. What is established is that the algorithm now matches and the device dependence is gone.Both bridge methods also decode through one
loadUprightImagehelper now, mirroring Android'sloadBitmap, so the tensor and the crop cannot disagree about EXIF orientation.Identity
MARKETING_VERSIONwas0.0.58while Android shipped0.1.0-field.6— the field.4, field.5 and field.6 bumps each touched onlyandroid/app/build.gradle. Version and build number now match Android, so the Settings screen reports the same string on both platforms. Debug builds take a.devbundle suffix the way Android already does, so a development build no longer replaces a tester's field build. The launch screen no longer reads "Off Grid", andCFBundleNameis EleBook rather than the Xcode target name.Permissions
Info.pliststill asked for the microphone "for voice-to-text transcription using Whisper" and for speech recognition, and described the photo library as being "to attach images to conversations". No audio dependency remains inpackage.jsonand the picker is photo-only (useCaptureFlow.ts:140-148), so both stale requests are removed and the photo-library string now describes what the app does. Android's manifest asks only for INTERNET, location, and VIBRATE.One unrelated fix, because it blocked the commit
The pre-commit hook pipes staged paths straight into
swiftlint lint, and passing explicit paths overrides theexcluded:list in.swiftlint.yml. So staging any Swift file lintedios/OffgridMobileTests/OffgridMobileTests.swifttoo and failed on 8 violations that already exist onwildlife-reid(force_try, identifier_name, file_length) — none of which any merge gate enforces, becausenpm run lint:iosand CI honour the exclude list and skip that file. It had not fired before because the recent iOS work was done on hosts without SwiftLint installed, where the hook prints a warning and skips.--force-excludemakes the hook agree with CI. Reverting that one line reproduces the failure.Type of Change
Screenshots / Screen Recordings
No screens changed — the shared TypeScript UI is untouched. The only visual difference is the iOS launch screen, which drops the "Off Grid" wordmark and centres the logo, matching
splash_background.xmlon Android.Checklist
General
Testing
npm test)Left unchecked deliberately.
npm testchains three suites and only two of them could run on this host: the JS suite (67 files, 1031 tests) and the iOS XCTest suite on an iPhone 17 Pro simulator, both green, plusnpx tsc --noEmitand SwiftLint. The Android suite needs a JDK and an Android SDK that this machine does not have, so CI is the first place it runs — no Kotlin changed. Neither app was launched on a device or an emulator, and no light/dark pass was done, because no shared TypeScript changed.React Native Specific
project.pbxproj)SPACING/TYPOGRAPHYconstants from the themeuseThemedStylespattern (not inline or staticStyleSheet.create)FlatList/FlashList(not.map()insideScrollView)No new native modules and no TypeScript touched, so everything below the first line is N/A rather than done.
Performance & Models
/vs\)N/A — no download or long-running task code changed.
Security
Additional Notes
Not verified on a device. Simulator unit tests and SwiftLint were run on macOS; nothing here was exercised on a physical iPhone, and no field capture was compared end to end against an Android capture of the same animal. A same-photo comparison across the two platforms is the check that would actually confirm parity, and it still needs doing.
The embedding change is behavioural. iOS tensors computed before this commit do not match tensors computed after it, and pre-existing iOS embeddings were themselves device-dependent. Any iOS-side candidate scores or cached crops from an earlier build should be regenerated rather than compared against new ones.
MARKETING_VERSIONnow carries a pre-release string.0.1.0-field.6is not a validCFBundleShortVersionStringfor App Store submission, which accepts at most three dot-separated integers. Ad-hoc and AltStore distribution are unaffected, andrelease-ios.ymlalready copied this same string straight out ofpackage.json, so this matches what the release path was always going to produce. Worth resolving before any TestFlight or App Store attempt.Nothing stops this drifting again. CI has an
android-buildjob that compiles Android debug and release, and no iOS equivalent — the iOS app target is only ever built as a side effect of the unit-test job. Anios-buildjob would be the durable fix; it is out of scope here because it costs macOS CI minutes on every PR.Deliberately left alone
x=1.5, width=2.5selects[1,3)on Android and[1,4)on iOS. Finding 4 ofkb/wildlife-reid-mobile/outputs/reports/2026-09-09-codex-exif-orientation-review.mddeferred this as pre-existing and wanting a shared integer-bound calculation with its own tests. It is a real remaining mismatch, roughly a pixel on a crop of several hundred, and folding it in here would have made the preprocessing change harder to read.CoreMLDiffusionModule(Stable Diffusion) andPDFExtractorModuleare still compiled into the iOS target with no Android counterpart and no remaining product use.ml-stable-diffusionis still a resolved Swift Package, so every iOS build downloads Apple's diffusion package to compile a module nothing calls. Removing them touchesproject.pbxprojand drops roughly 30 XCTest cases, which belongs in its own PR.DownloadManagerModuleis compiled but has no JS binding at all (noted infileDownloadService/index.ts:18).ios/OffgridMobile/{CoreMLDiffusion,Download,ImageTensor,PDFExtractor}/are orphaned duplicates that the Xcode project does not reference.TODO.mdstill describes the replaced chat and image-generation product.