iOS¶
GeoLibre runs as a native iOS app built from the same React codebase via Tauri v2 mobile — no separate app. The webview UI (WKWebView) is bundled in the app, so the shell works offline; map tiles and the heavier engines are fetched on demand (same as the desktop and Android builds).
Status: builds and signs, locally and in CI; not yet shipped. The pipeline has been run end to end both ways and produces a submittable
.ipa— bundle idorg.geolibre.app, signed by Apple Distribution,get-task-allow=false, an embedded App Store provisioning profile, the merged location usage string, and the web assets embedded in the binary.
- Locally on a Mac (Xcode 26.6 / iOS SDK 26.5):
tauri ios init→ unsigned archive →xcodebuild -exportArchive.- In CI, the signed job has run green on a
macos-15runner (Xcode 26.3): it imported the certificate and profile from theAPPLE_IOS_*secrets, archived, exported under manual signing, verified the signature, and published thegeolibre-ios-ipaartifact.A first build (version 2.4.0, build 2.4.0) has been uploaded to App Store Connect by hand via Transporter. What has not happened: any submission for review. Everything below still has to be run on a Mac (iOS cannot be cross-compiled from Linux).
What works on iOS vs desktop¶
Same split as Android. The iOS build ships the full map workspace, Add Data, the Vector tools (Turf.js / in-browser GeoPandas via Pyodide), the SQL Workspace (DuckDB-WASM and PGlite/PostGIS), the Python Console (Pyodide), geocoding, statistics, the AI assistant, story maps, and plugins.
Tools that depend on a local desktop process are hidden on mobile because iOS has no Python sidecar or local helper binaries and its sandbox forbids spawning subprocesses:
- Processing → Whitebox, Raster, Conversion, AI Segmentation (all need the Python sidecar)
- Add Data → PostgreSQL (served by the local Martin tile server)
These are gated by a user-agent isMobile() check (which already matches
iPhone/iPad) so they never appear and then fail. Everything else runs
client-side.
Location permission (required)¶
This is the one place iOS differs sharply from Android. Android's geolocation
plugin declares the runtime permission and the OS shows a generic dialog; iOS
terminates the app the instant it requests location if no usage-description
string is present. GeoLibre supplies it in
src-tauri/Info.ios.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>GeoLibre uses your location to center the map, capture GPS points during Field Collection, and record GPS tracks.</string>
Tauri merges Info.ios.plist into the generated
gen/apple/geolibre-desktop_iOS/Info.plist at build time. gen/apple is git-ignored, so
this file is the durable home for the string — the same reason Android's manifest
permissions come from the plugin rather than a hand-edited, regenerated manifest.
It covers all three location consumers: Field Collection, GPS Tracking, and the
Controls → GeoLocate map control. After a build, confirm the key survived the
merge (see Build below).
Toolchain setup (one time)¶
You need a Mac with Xcode (from the App Store; open it once to accept the license and install the iOS platform), the command-line tools, CocoaPods, and the Rust iOS targets.
xcode-select --install # command-line tools (if not already)
sudo xcodebuild -license accept
brew install cocoapods # or `gem install cocoapods`
# Rust device + simulator targets (install rustup first if needed)
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
aarch64-apple-ios is real devices and the App Store .ipa;
aarch64-apple-ios-sim is the simulator on Apple Silicon Macs.
Build¶
cd apps/geolibre-desktop
npx tauri ios init # generate src-tauri/gen/apple (once)
npx tauri ios dev # run in the simulator / on a tethered device
npx tauri ios build --no-sign # unsigned .ipa + .xcarchive (no Apple account needed)
npx tauri ios build --export-method app-store-connectdoes not work as-is. The projecttauri ios initgenerates bakesCODE_SIGN_IDENTITY = "iPhone Developer"into both the debug and release configurations, so Xcode resolves development signing even when the export method is App Store. Automatic development signing then demands a registered device and fails with:error: Your team has no devices from which to generate a provisioning profile. error: No profiles for 'org.geolibre.app' were found: Xcode couldn't find any iOS App Development provisioning profiles matching 'org.geolibre.app'.Overriding
CODE_SIGN_IDENTITY="Apple Distribution"does not help either — Xcode rejects it as "automatically signed for development, but a conflicting code signing identity … has been manually specified."Split the archive from the export instead. Archive unsigned, then let the export step do the distribution signing — this needs no registered device, and Xcode creates the Apple Distribution certificate, registers the App ID, and generates the App Store profile on the fly:
cd apps/geolibre-desktop npx tauri ios build --no-sign # → gen/apple/build/geolibre-desktop_iOS.xcarchive G=src-tauri/gen/apple cat > "$G/ExportOptions-appstore.plist" <<'PLIST' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"><dict> <key>method</key><string>app-store-connect</string> <key>teamID</key><string>YOUR_TEAM_ID</string> <key>signingStyle</key><string>automatic</string> <key>uploadSymbols</key><true/> </dict></plist> PLIST xcodebuild -exportArchive -allowProvisioningUpdates \ -archivePath "$G/build/geolibre-desktop_iOS.xcarchive" \ -exportOptionsPlist "$G/ExportOptions-appstore.plist" \ -exportPath "$G/build/appstore"This produces a genuinely submittable
build/appstore/GeoLibre.ipa: signed byApple Distribution,get-task-allow=false, entitlement<TEAM>.org.geolibre.app, and an embedded iOS Team Store Provisioning Profile. Verify withcodesign -dvvvbefore uploading.Note the certificate Xcode creates this way is Cloud Managed, so it does not appear in
security find-identity -v -p codesigning. An empty identity list is not evidence the signing failed — check the.ipaitself.
gen/appleis generated (git-ignored) and regenerated on demand.initalso mergestauri.ios.conf.json(bundle id, drops the Python backend) andInfo.ios.plist(the location string).- The app is named GeoLibre on iOS (the desktop build is "GeoLibre Desktop")
and uses the bundle id
org.geolibre.app, both set viasrc-tauri/tauri.ios.conf.json— the same override pattern and reasoning as Android (identifierintauri.conf.jsonstaysorg.geolibre.desktopso it keeps keying desktop settings and the Linux/macOS packaging). - Verify the location string landed after a build:
/usr/libexec/PlistBuddy -c 'Print :NSLocationWhenInUseUsageDescription' \
src-tauri/gen/apple/geolibre-desktop_iOS/Info.plist
The merge happens during tauri ios build/dev, not during ios init —
straight after init the generated plist does not contain the key yet, and
that is expected. Note also that the Xcode project, target, and plist
directory are named from the Cargo package (geolibre-desktop in
src-tauri/Cargo.toml), not from productName. The user-visible app name and
bundle id still come from tauri.ios.conf.json — they land as PRODUCT_NAME:
GeoLibre and PRODUCT_BUNDLE_IDENTIFIER: org.geolibre.app in the generated
project.yml.
Signing¶
Every build that runs on a real device or reaches the App Store must be signed — there is no debug-keystore shortcut like Android's. You need:
- An Apple Developer Program membership ($99/year).
- A signing certificate (Apple Distribution for App Store; Apple Development
for device testing), exported from Keychain as a
.p12. - A provisioning profile for the
org.geolibre.appapp id. - Your Team ID (App Store Connect → Membership).
Locally, opening gen/apple/geolibre-desktop.xcodeproj in Xcode once and enabling
"Automatically manage signing" with your team is the simplest path. For CI, the
identity is imported from secrets (below).
The local and CI identities are not interchangeable. The export recipe above lets Xcode create a Cloud Managed Apple Distribution certificate and an Xcode-managed profile. Neither can be fed to CI: a cloud-managed identity has no exportable private key, so there is no
.p12to base64, and CI's manual-signing export rejects an Xcode-managed profile outright ("is Xcode managed, but signing settings require a manually managed profile"). For CI you must create a separate, manually managed pair in the Developer portal — an Apple Distribution certificate from a CSR you generate in Keychain Access (so you hold the private key and can export the.p12), plus an App Store provisioning profile fororg.geolibre.appbound to it. Holding both a cloud-managed and a manual distribution certificate at once is fine and normal.
Continuous integration¶
.github/workflows/ios.yml runs on macos-15 on each published GitHub release
(and on demand via "Run workflow"). Because iOS can't be cross-compiled from
Linux, this is the only mobile workflow that needs a macOS runner. The image is
not free to choose — see the Xcode floor below.
- With Apple signing secrets set, it imports the identity into a throwaway
keychain, archives unsigned, exports a signed
.ipaunder manual signing, verifies the bundle id and the signature, and uploads it as thegeolibre-ios-ipaartifact: APPLE_IOS_CERTIFICATE_BASE64—base64 -i dist.p12APPLE_IOS_CERTIFICATE_PASSWORDAPPLE_IOS_PROVISIONING_PROFILE_BASE64—base64 -i profile.mobileprovisionAPPLE_TEAM_ID— reused from the existing macOS/Homebrew signing secrets; the Team ID is account-wide.- Without them, it falls back to a no-signing compile check
(
cargo build --lib --target aarch64-apple-ios) so CI still catches iOS build breakage; it just can't produce an installable.ipa.
Reusing the macOS/Homebrew signing secrets?¶
Only the Team ID. The repo's release workflow already signs and notarizes the
macOS build for the Homebrew cask, but its APPLE_CERTIFICATE is a Developer ID
Application certificate — that type signs macOS apps distributed outside the
App Store and cannot sign an iOS app. iOS needs its own Apple Distribution
certificate and a provisioning profile (Developer ID distribution has
neither), so those are the APPLE_IOS_* secrets above, deliberately named apart
from the macOS ones to avoid feeding in the wrong cert/password. The good news:
they're created under the same paid Apple Developer account at no extra cost —
add an Apple Distribution certificate and an App Store provisioning profile for
org.geolibre.app in the Developer portal, and reuse the existing APPLE_TEAM_ID.
Why the job archives and exports as two steps. It archives with
npx tauri ios build --no-signand then signs in a separatexcodebuild -exportArchivestep using manual signing, for the reason given under Build:tauri ios build --export-methodcannot sign this project, because the generated project bakes inCODE_SIGN_IDENTITY = "iPhone Developer"and automatic signing therefore hunts for a development profile no matter what the export method says.This has been verified on a runner — a
workflow_dispatchrun withexport_method: app-store-connectcompleted green, reportingIPA ... bundle id org.geolibre.app, signed by Apple Distributionand publishing thegeolibre-ios-ipaartifact. What remains unexercised is the release-triggered entry point (the job has only been started manually) and everything downstream of the artifact: no build has been uploaded to App Store Connect.The runner's Xcode sets a hard floor.
tauri ios initgenerates an Xcode project in object format 77, which only Xcode 16+ can open. Onmacos-14the archive failed immediately, before compiling anything:xcodebuild: error: Unable to load workspace '.../geolibre-desktop.xcodeproj/project.xcworkspace/'. Reason: The project 'geolibre-desktop' cannot be opened because it is in a future Xcode project file format (77).The job therefore runs on
macos-15and explicitly selects the newest Xcode on the image rather than trusting its default, failing early with a clear message if that is older than 16. Note this floor comes from the Tauri CLI's generated project, not from anything in this repo — re-check it whenever the Tauri CLI is bumped.Separately, App Store Connect rejects uploads built against an SDK below its current floor. That failure is silent at build time — an older-but-openable Xcode still produces an
.ipaand only fails at upload. The Xcode version and iOS SDK list are printed into the run log so a rejected upload can be diagnosed from the run instead of guessed at.
The workflow_dispatch export_method input picks the export path
(app-store-connect for TestFlight/App Store, release-testing for ad-hoc
registered devices, debugging for development).
Install / test¶
- Simulator:
npx tauri ios devand pick a simulator, or open the Xcode project and Run. No paid account needed for the simulator. - Your own device: tether it, open
gen/apple/geolibre-desktop.xcodeprojin Xcode, select the device, and Run (a free Apple ID allows 7-day device signing). - Testers: distribute a signed build through TestFlight (upload the
.ipavia Xcode Organizer or Transporter, then invite testers in App Store Connect).
Publishing to the App Store¶
The build side is covered by the CI workflow; the rest is App Store Connect onboarding.
- App record. In App Store Connect, create a new app with the bundle id
org.geolibre.app(register the app id in the Developer portal first). - Minimum Functionality (Guideline 4.2). Apple rejects apps that are "a
repackaged website." GeoLibre passes because it's a bundled native app with
real device integration — GPS, offline-capable map workspace, local file
handling — not a wrapper that loads a remote URL. Keep it that way: ship the
web assets in the binary (the default here), don't point the webview at
geolibre.app. - Upload the
.ipafrom thegeolibre-ios-ipaCI artifact (or via Transporter / Xcode Organizer) to a TestFlight build, then submit that build for App Store review.
Build numbers. App Store Connect consumes a CFBundleVersion
permanently per marketing version, and each upload must also sort above the
last one consumed, so every upload needs a fresh, higher value. CI stamps the
archive's CFBundleVersion with $GITHUB_RUN_NUMBER — an integer that only
ever rises — while CFBundleShortVersionString keeps tracking
tauri.conf.json and stays the version users see.
Re-running an existing run appends $GITHUB_RUN_ATTEMPT (42 becomes
42.2), because the run number itself is fixed for a run's lifetime: without
the suffix, re-running a run whose IPA was already uploaded would regenerate
the same build number. The one hand-uploaded 2.4.0 build used 2.4.0, which
a bare run number of 3 or more already beats under Apple's dotted-integer
comparison.
This is deliberately not done with tauri ios build --build-number:
that flag appends to the version, producing e.g. 2.4.0.42. A
CFBundleVersion may hold at most three period-separated integers, so
App Store Connect rejects a four-component value at upload
(ITMS-90060) — after the whole build has run. The verify step asserts the
shape for that reason.
Uploading by hand (no CI) means setting the build number yourself if the
marketing version has been uploaded before.
4. Store listing: icon (already generated under src-tauri/icons/ios),
screenshots for the required device sizes (6.7" and 6.5" iPhone, plus 12.9"
iPad — a GIS workspace is genuinely iPad-appropriate), description, keywords.
5. Privacy. Fill the App Privacy questionnaire honestly — declare each
network destination (geocoding, the AI assistant, basemap/tile fetches, Google
OAuth for Earth Engine) and that location is used when in use and not
collected by a backend. Point the privacy policy URL at the published
privacy policy.
6. Age rating questionnaire and category (Navigation or Productivity).
7. Export compliance. Already declared in src-tauri/Info.ios.plist:
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
App Store Connect asks about encryption on every upload, and a build with the question unanswered cannot be submitted or sent to testers ("Missing Compliance"). Declaring it in the plist answers it once, at build time.
false asserts that GeoLibre's use of encryption stays limited to what Apple
treats as exempt — HTTPS/TLS via the system libraries for tiles, geocoding,
the AI assistant, cloud catalogs, and the collaboration relay — and that the
app ships no cryptography of its own. That holds today, but it is a legal
declaration, not a build flag: revisit it if that ever changes.
Builds uploaded before this key was added (the first 2.4.0 upload) still need the question answered by hand in App Store Connect.
Known limitations / follow-ups¶
Most mirror Android:
- Local-file sources (MBTiles, local rasters, project files) assume real filesystem paths; iOS hands apps sandboxed URLs via the document picker, so those flows need adapting before they work natively.
- The Download Offline Area tool relies on a service worker, which the Tauri builds don't use — it's a PWA feature. Native offline basemap caching is future work.
- Earth Engine OAuth uses a desktop loopback/multi-window flow; a mobile deep-link redirect (an iOS URL scheme / universal link) is future work.
- iPadOS multitasking (Split View / Stage Manager) hasn't been tuned; the responsive layout should adapt, but verify on a real iPad before release.