iOS
By the end of this guide your Lua project runs on your own iPhone, launched from the home screen.
Read this first. iOS is the hardest platform Natiny supports, and most of the difficulty has nothing to do with the engine. Apple requires every app on a device to be signed by a registered developer, so half of this guide is about certificates, provisioning profiles and Xcode. The guide assumes you already know what code signing is and how apps reach a device. If you have never signed anything before, expect to spend an hour here, and expect to use Xcode a little even though Natiny never asks you to.
Everything below happens on a Mac. There is no way around that: the tools that sign an app are Apple's and run nowhere else.
How an iOS build is split in two
Every other platform gives you a file you can run. iOS gives you an archive, because a signature has to be added before a phone will accept the app - and a signature needs a certificate that identifies you. That certificate lives in your keychain and cannot travel inside a download.
So the work is split:
we cross-compile → libnatiny_player_ios_arm64.a (in the bundle)
you link, pack and sign → MyGame.app (on your Mac)
make_app.py, which ships inside the bundle, does the second half. You will
run it twice: once unsigned to check the toolchain, and once for real.
Check that your Mac can build for iOS
You need Xcode - the application, not only the command-line tools. The tools
carry the macOS SDK and no other, so a Mac can have clang, ld and codesign
and still be unable to compile for a phone.
xcrun --sdk iphoneos --show-sdk-path
A path means you are ready. If it says SDK "iphoneos" cannot be located,
install Xcode from the App Store, open it once so it finishes installing its
components, then point the tools at it:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
Python 3.8 or newer is the only other requirement, and macOS already has it.
Unzip the bundle
Download the Lua wrapper bundle for ios-arm64 and unzip it. Four things
matter:
natiny-ios_arm64-app/
├── libnatiny_player_ios_arm64.a the interpreter, the engine and Lua
├── make_app.py turns that archive into an .app
├── Info.plist.in the template it fills in
└── data/
└── main.lua the script that starts
data/ is the example project. Later you will point the packer at your own
folder; for now the example is what we want, because if something goes wrong
you will know it is not your Lua code.
Build an unsigned app to check the toolchain
Run the packer with nothing but a project and a name. It finds the archive and the template beside itself:
cd natiny-ios_arm64-app
python3 make_app.py --project ./data --name "My Game" \
--bundle-id com.example.mygame
You should see the link command, then a line like:
-> /Users/you/natiny-ios_arm64-app/My Game.app 1.3 MB com.example.mygame 1.0
My Game.app is a real app bundle: your executable, your data/ folder and
an Info.plist. What it is not is installable. Without --identity the packer
signs it ad-hoc, which no device accepts.
That is worth stating plainly, because the next thought is usually the
Simulator: this bundle will not run there either. It is built for
arm64-apple-ios, and the Simulator runs arm64-apple-ios-simulator - a
different platform, compiled against a different SDK. Natiny has no simulator
build. On an Apple silicon Mac the Simulator may even install it and then
crash on launch, which looks like a bug in the app and is not.
So: the toolchain works, and now we make the app installable.
Register an app identifier with Apple
An app on a device is identified by its bundle identifier, and Apple keeps one global namespace of them. Your identifier has to be unused.
Warning:
com.example.mygame, used above and in every example on this page, is taken. So is anything else starting withcom.example. Xcode will answer "cannot be registered to your development team because it is not available". Derive your own from a domain or an account you control -ru.yourname.mygameworks.
Pick yours now. It has to be the same string in three places: the Xcode
project below, the provisioning profile, and --bundle-id.
Get a certificate and a profile through Xcode
Two things have to exist before a phone will run your app:
- a certificate, which proves the app was signed by a registered developer;
- a provisioning profile, which says this app identifier may run on this particular device.
A free Apple ID gets you both. Xcode is what issues them, and the shortest path is to let it do that once for a throwaway project.
- Open Xcode → Settings → Accounts and add your Apple ID. Xcode creates an Apple Development certificate in your keychain.
- Connect the iPhone with a cable, unlock it, and tap Trust This Computer.
- On the phone, turn on Settings → Privacy & Security → Developer Mode and let it restart. Without it, a development-signed app will not install at all.
- In Xcode: File → New → Project → iOS → App. Any product name will do, but set Bundle Identifier to the one you chose, and set Team to your personal team.
- Select your iPhone as the run destination and press Run (⌘R).
That last step is the one that matters. Building to a connected device is what makes Xcode register the device and issue a profile for your identifier. When the throwaway app appears on the phone, you can close the project and never open it again.
Find the two names the packer asks for
The certificate:
security find-identity -v -p codesigning
1) 4738DCEAD9968002836BCAC7DFD05A619052CCAD "Apple Development: you@mail.com (77UNP8U994)"
Either half works: the long hash, or the name in quotes. The hash has no spaces in it, so it is harder to mistype.
The profile is a file on disk. Xcode 16 keeps profiles in one place and earlier versions in another, so look in both, and print which identifier each one covers:
for d in ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles \
~/Library/MobileDevice/Provisioning\ Profiles; do
[ -d "$d" ] || continue
for p in "$d"/*.mobileprovision; do
echo "$p -> $(security cms -D -i "$p" | plutil -extract Entitlements.application-identifier raw -)"
done
done
Each line ends with TEAMID.your.bundle.id. Copy the one matching your
identifier next to the bundle:
cp <that-file>.mobileprovision ~/natiny-ios_arm64-app/mygame.mobileprovision
If both folders are empty, step 5 above did not happen. Adding an Apple ID creates the certificate; only a build to a connected device creates the profile.
Build the signed app
Same command as before, plus the two you just found:
python3 make_app.py --project ./data --name "My Game" \
--bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision
The packer links the archive, writes Info.plist, copies data/ in, embeds
the profile, reads the entitlements out of it the way Xcode does, and signs
the bundle. It checks the identity against your keychain before it starts, so
a mistyped name fails immediately instead of after the build.
Install it on the phone
devicectl ships with Xcode 15 and newer:
xcrun devicectl list devices
xcrun devicectl device install app --device <UDID> "My Game.app"
On older Xcode, open Window → Devices and Simulators, select the phone,
and drag the .app onto Installed Apps.
The app is now on the home screen with a plain grey icon - we have not given it one yet.
Run it and read what it prints
The first launch of an app signed with a free account is refused: the phone does not trust your certificate yet. Go to Settings → General → VPN & Device Management → Developer App and trust it. Then tap the icon.
The Natiny logo appears on a dark background. That is the example
data/main.lua, and it means the whole chain works: cross-compiled engine,
your signature, your device.
When something goes wrong instead, launch it from the Mac so you can see why:
xcrun devicectl device process launch --console --device <UDID> ru.yourname.mygame
--console prints the program's output in your terminal. A crash with no
Natiny output at all is usually a signing problem; output that stops partway
is your Lua code.
Put your own project in the app
Everything under --project becomes data/ inside the bundle, and
data/main.lua is what starts:
my_game/
├── main.lua
├── player.lua
└── fonts/
└── roboto.ttf
python3 make_app.py --project ./my_game --name "My Game" \
--bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision
Resource paths do not change between platforms:
local font_data = natiny.resource.load("data/fonts/roboto.ttf")
An app bundle is read-only, so files written at run time go to the app's
Documents folder instead. The resource API knows that and looks there when
loading, so a save and the load after it agree about where the file went:
natiny.resource.save(save_resource, "save.json")
local loaded = natiny.resource.load("save.json")
Nothing is compiled when you repack, so this step takes about a second.
Give it an icon, a version and a real name
python3 make_app.py --project ./my_game --name "My Game" \
--bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision \
--icon icon.png --version 1.2.0 --build 7
--icontakes a square PNG, 120×120 or larger. Without it you get the grey square you saw above.--versionis what the App Store shows;--buildis the number that must increase with every upload.--ipa MyGame.ipaadditionally writes the zipped bundle that App Store Connect and device installers take. You do not need it for cable installs.
python3 make_app.py --help lists the rest.
Typical problems
SDK "iphoneos" cannot be located- Xcode is missing, or installed and not selected. See the first section.cannot be registered to your development team- the bundle identifier is taken. Choose one of your own.no signing identity matches ...- the string does not match your keychain. The packer prints what you do have; copy one of those.- The install fails - Developer Mode is off, the profile does not cover this identifier, or the profile does not list this device.
- The app installs but will not open - the certificate is not trusted on the phone yet.
- The app stops launching after about a week - a free account's profile expired. Rebuild and reinstall; nothing else changes.
What you have now
Your Lua project runs on a real iPhone, and the loop is short: edit the
scripts, rerun make_app.py, reinstall. The engine never has to be rebuilt,
because the archive in the bundle already contains it.
A touch arrives as the left mouse button at the finger's position, so a
project written for a desktop works on a phone as it stands. For more than one
finger, natiny.input reports every touch.