# 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: scaffolding.** The iOS config, `Info.ios.plist`, and CI workflow are
> in place, but — unlike Android — no iOS build has been shipped yet. Everything
> below has to be run and verified on a Mac (iOS cannot be cross-compiled from
> Linux). Treat the first `tauri ios build` as a bring-up, not a routine build.
## 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`:
```xml
NSLocationWhenInUseUsageDescription
GeoLibre uses your location to center the map, capture GPS points during Field Collection, and record GPS tracks.
```
Tauri merges `Info.ios.plist` into the generated
`gen/apple/geolibre_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.
```bash
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
```bash
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 # release archive → signed .ipa (needs signing, below)
```
- `gen/apple` is generated (git-ignored) and regenerated on demand. `init` also
merges `tauri.ios.conf.json` (bundle id, drops the Python backend) and
`Info.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 via
`src-tauri/tauri.ios.conf.json` — the same override pattern and reasoning as
Android (`identifier` in `tauri.conf.json` stays `org.geolibre.desktop` so it
keeps keying desktop settings and the Linux/macOS packaging).
- Verify the location string landed after a build:
```bash
/usr/libexec/PlistBuddy -c 'Print :NSLocationWhenInUseUsageDescription' \
src-tauri/gen/apple/geolibre_iOS/Info.plist
```
## 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:
1. An **Apple Developer Program** membership ($99/year).
2. A **signing certificate** (Apple Distribution for App Store; Apple Development
for device testing), exported from Keychain as a `.p12`.
3. A **provisioning profile** for the `org.geolibre.app` app id.
4. Your **Team ID** (App Store Connect → Membership).
Locally, opening `gen/apple/geolibre.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).
## Continuous integration
`.github/workflows/ios.yml` runs on `macos-14` 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.
- **With Apple signing secrets set**, it imports the identity into a throwaway
keychain, archives, exports a signed `.ipa`, verifies its bundle id, and
uploads it as the `geolibre-ios-ipa` artifact:
- `APPLE_IOS_CERTIFICATE_BASE64` — `base64 -i dist.p12`
- `APPLE_IOS_CERTIFICATE_PASSWORD`
- `APPLE_IOS_PROVISIONING_PROFILE_BASE64` — `base64 -i profile.mobileprovision`
- `APPLE_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`.
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 dev` and 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.xcodeproj` in Xcode,
select the device, and Run (a free Apple ID allows 7-day device signing).
- **Testers:** distribute a signed build through **TestFlight** (upload the `.ipa`
via 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.
1. **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).
2. **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`.
3. **Upload** the `.ipa` from the `geolibre-ios-ipa` CI artifact (or Xcode) to a
TestFlight build, then submit that build for App Store review. The build
number (`CFBundleVersion`) is derived from the version in `tauri.conf.json`
and must increase on every upload.
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](privacy.md).
6. **Age rating** questionnaire and category (Navigation or Productivity).
## 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.