# iOS

> How to run a Moonspell engine game as a normal Expo development build on an iOS simulator or iPhone, what app.json needs, release builds, and what differs from playing inside the Moonspell app.

> **Not built on the authors' machine.** `npm run ios` (prebuild, CocoaPods, Xcode, a simulator or an iPhone) was never run for this page. What was run: `npx expo config` (the config is valid and the `react-native-webgpu` plugin resolves), `npx expo prebuild --no-install --clean` in a throwaway copy of the template (it succeeded), and `npx expo export --platform ios` (the JavaScript bundle builds). It is not known from our own runs whether the app launches, whether WebGPU draws in the simulator, or how fast it runs. The steps below are Expo's standard ones, checked against Expo SDK 58's docs and the template's config.

On iOS your game is an ordinary Expo app: `expo run:ios` generates the native project, builds it with Xcode, installs it and loads the game's JavaScript from Metro. The engine needs nothing from the Moonspell app to run there ([What is and is not open](https://docs.moonspell.app/start/open-source.md)).

## Prerequisites

- A Mac with Xcode and an iOS simulator, or an iPhone with a cable. `expo run:ios` only runs on a Mac with Xcode installed.
- CocoaPods (prebuild installs the pods).
- Node and npm, and a real `npm install` in the game's folder.
- For an iPhone: an Apple account in Xcode with a signing team, and a bundle identifier of your own (below). On iOS 16 and later the phone needs Developer Mode on ([Expo: iOS developer mode](https://docs.expo.dev/guides/ios-developer-mode/)).

## Run it

```bash
npm install
npm run ios
```

`npm run ios` is `expo run:ios`. The first run generates `ios/` (Expo's prebuild), installs the pods, compiles a debug build, installs it on a simulator and starts Metro. Later runs skip prebuild while `ios/` exists. Arguments after `--` go to `expo run:ios`:

```bash
npm run ios -- --device
```

`--device` lets you pick a connected iPhone or a simulator.

The template does not include `expo-dev-client`, so this is a plain debug build that loads from Metro, without the development launcher screen. Add `expo-dev-client` only if you want that launcher. React Native 0.88 always runs the New Architecture; there is no flag to set.

## What `app.json` needs

The template's `app.json`:

```json
{
  "expo": {
    "name": "Sunset Sprint",
    "slug": "sunset-sprint",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "dark",
    "backgroundColor": "#07070C",
    "ios": { "bundleIdentifier": "com.rofi.crescentmoon.game" },
    "android": { "package": "com.rofi.crescentmoon.game" },
    "plugins": ["react-native-webgpu"],
    "extra": { "genux": { "backgroundColor": "#07070C" } }
  }
}
```

| Key | Why |
| --- | --- |
| `plugins: ["react-native-webgpu"]` | react-native-webgpu's config plugin. On iOS it turns Metal API validation off in the Xcode scheme's Run action (the prebuild check showed `enableGPUValidationMode = "1"`). The library's docs say validation can raise false positives in Dawn that crash the app on simulators. Keep it |
| `ios.bundleIdentifier` | Change `com.rofi.crescentmoon.game` to an id of your own before you build for a phone: Xcode makes the provisioning profile for this id |
| `orientation: "portrait"` | Locks the app to portrait, the shape the engine's HUDs and controls are laid out for |
| `name` | The app's name on the home screen |
| `extra.genux` | Not read by the engine. Harmless in your build |

### Minimum iOS version

The template does not set a deployment target, so prebuild uses Expo SDK 58's default: iOS 16.4 in the prebuild check. That is above what the engine's native libraries ask for (Skia's podspec says iOS 14.0; react-native-webgpu, MMKV and Reanimated follow React Native's minimum).

### Deep links

The template sets no `scheme`, so your build opens no links. `useLaunchRoomId()` falls back to React Native's `Linking`, so if you add an Expo `scheme` to `app.json`, a link with `?room=CODE` that opens the app drops the player into that room ([Net](https://docs.moonspell.app/engine/net.md)). Not tested.

## On an iPhone

```bash
npm run ios -- --device
```

Pick the connected phone. Set `ios.bundleIdentifier` to your own id first, so Xcode can make a provisioning profile for it, and turn on Developer Mode on the phone. For signing and local builds in general, follow [Expo: local app development](https://docs.expo.dev/guides/local-app-development/); this page does not repeat it.

react-native-webgpu's docs say the Metal validation false positives do not happen on physical devices, and recommend testing on hardware for real performance.

## Release builds

At a pointer level only; none of this was run for the engine:

- Locally: `npx expo run:ios --configuration Release` makes a release build, unsigned. Signing it for the App Store is covered by [Expo: local app production](https://docs.expo.dev/guides/local-app-production/). You can also open `ios/*.xcworkspace` in Xcode and archive from there.
- In the cloud: EAS Build (`eas build --platform ios`), which handles signing credentials ([Expo: EAS Build](https://docs.expo.dev/build/introduction/)). Expo's development-build profiles expect `expo-dev-client`, which the template does not include.

## If reloads crash

A development build runs the upstream versions of Skia and react-native-audio-api. The Moonspell app carries its own fixes for two teardown problems in them (Skia releasing the Metal device too early, and react-native-audio-api calling a nil render block). With one game per app they should matter only when the JavaScript reloads, for example on save in development. If repeated reloads crash your build, suspect these first. Untested.

## Different from the Moonspell app

| | Inside the Moonspell app | Your Expo build |
| --- | --- | --- |
| How the game loads | The app loads the game's export into its own runtime | Your app is the game; Metro serves it in development |
| Exit button | Back to the app's games | `exitToShell()` does nothing; the template's `App.tsx` unmounts the game and shows its "Play" button |
| Paused off screen | Yes, while the app's feed keeps the game loaded but not shown | Never: the game is always active |
| `getLaunchUrl()`, `getShareUrl()`, `roomInviteUrl()` | The app passes the links in | `null` |
| Live updates, App Clip, feed video | Yes | No |
| Game backend | The game's own Worker | `GAME_BACKEND_URL` is `null` in the template: leaderboards, presence and rooms stay on this device until you deploy the backend and set it |

On the phones the shell is the optional native module `GenUXExpoModule`; in your build `requireOptionalNativeModule` returns `null` and the engine runs on its own ([Platform layer](https://docs.moonspell.app/engine/platform-layer.md)). All the differences between platforms are on [Platform differences](https://docs.moonspell.app/platforms/differences.md).

## See also

- [Android](https://docs.moonspell.app/platforms/android.md): the same for Android.
- [Web](https://docs.moonspell.app/platforms/web.md): the fastest way to work on a game.
- [Quickstart](https://docs.moonspell.app/start/quickstart.md)
