Libgdx ios robovm
Claude Code skills for working with the libGDX framework
npx -y skills add kyu-n/gdx-claude-skills --skill libgdx-ios-robovmAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing libGDX Java/Kotlin code targeting iOS via RoboVM — IOSApplication launcher, IOSApplicationConfiguration, robovm.xml, robovm.properties, RoboVM reflection limitations (forceLinkClasses), iOS lifecycle, file access on iOS, safe area insets for notch/Dynamic Island, screen density, haptics, or on-screen keyboard. Use when debugging classes missing at runtime on iOS, black screen on launch, or UI hidden behind the notch.
SKILL.md
12.4 KB, as published. Nobody here has run it
libGDX iOS / RoboVM Backend
Reference for the iOS backend using RoboVM. Covers launcher setup, RoboVM compilation model, lifecycle, file access, and platform quirks.
Launcher
import org.robovm.apple.foundation.NSAutoreleasePool;
import org.robovm.apple.uikit.UIApplication;
import com.badlogic.gdx.backends.iosrobovm.IOSApplication;
import com.badlogic.gdx.backends.iosrobovm.IOSApplicationConfiguration;
public class IOSLauncher extends IOSApplication.Delegate {
@Override
protected IOSApplication createApplication() {
IOSApplicationConfiguration config = new IOSApplicationConfiguration();
config.orientationLandscape = true;
config.orientationPortrait = false;
config.useAccelerometer = false; // disable if unused — saves battery
config.useCompass = false;
config.preferredFramesPerSecond = 60; // 0 = max (default)
config.useHaptics = true; // REQUIRED for Gdx.input.vibrate() on iOS
return new IOSApplication(new MyGame(), config);
}
public static void main(String[] argv) {
NSAutoreleasePool pool = new NSAutoreleasePool();
UIApplication.main(argv, null, IOSLauncher.class);
pool.close();
}
}
IOSApplicationConfiguration
| Option | Type | Default | Notes |
|---|---|---|---|
orientationLandscape | boolean | true | Enable landscape orientations |
orientationPortrait | boolean | true | Enable portrait orientations |
useAccelerometer | boolean | true | Disable to save battery if unused |
useCompass | boolean | true | Disable to save battery if unused |
preferredFramesPerSecond | int | 0 | Target FPS. 0 = max supported by screen (typically 60). Set 30 for battery savings. |
useHaptics | boolean | false | Must be true for Gdx.input.vibrate() to work |
Backend Variants: Classic vs MetalANGLE
libGDX provides two iOS backend options (both use RoboVM):
Note: Multi-OS Engine (MOE) and gdx-backend-moe are defunct — do NOT recommend them. The only supported iOS backends are RoboVM-based.
Classic (ios-robovm) | MetalANGLE (ios-robovm-metalangle) | |
|---|---|---|
| Graphics | OpenGL ES directly | OpenGL ES → Metal translation |
| Status | Works, but OpenGL ES deprecated by Apple (iOS 12, 2018) | Recommended for new projects (libGDX 1.12+) |
| Gradle dependency | gdx-backend-robovm | gdx-backend-robovm-metalangle |
| robovm.xml framework | OpenGLES | Metal (no OpenGLES) |
| Game code changes | — | None — same libGDX API |
MetalANGLE translates OpenGL ES calls to Metal transparently. Your game code stays identical — only the backend dependency and robovm.xml frameworks differ. The launcher class (IOSApplication.Delegate) does not change.
For new projects, use gdx-backend-robovm-metalangle. Apple could remove OpenGL ES in a future iOS version. MetalANGLE provides forward-compatibility with no code changes.
Migrating from classic to MetalANGLE: Change the Gradle dependency and swap OpenGLES → Metal in robovm.xml frameworks. No game code changes required.
Apple Silicon Macs: The classic ios-robovm backend does not support arm64 iOS simulators. On Apple Silicon Macs the default simulator is arm64, so the classic backend fails. Either use an x86_64 simulator (Rosetta) or switch to the MetalANGLE backend, which supports arm64 simulators. This is another reason MetalANGLE is recommended for new projects.
RoboVM: How It Works
RoboVM compiles Java bytecode ahead-of-time (AOT) to native ARM. There is no JVM on the device. This means:
- All code must be known at compile time
- No dynamic class loading —
Class.forName()with runtime-determined strings fails - Unreferenced classes are stripped — classes only used via reflection are removed
- Standard Java collections, math, strings, threading all work fine
- JVM-specific features like
Runtime.exec(),ProcessBuilderdo not work (iOS sandbox)
robovm.xml
Main RoboVM configuration file. Key sections:
<config>
<!-- Frameworks: choose ONE set based on your backend -->
<!-- MetalANGLE backend (recommended): -->
<frameworks>
<framework>UIKit</framework>
<framework>Metal</framework>
<framework>QuartzCore</framework>
<framework>CoreGraphics</framework>
<framework>OpenAL</framework>
<framework>AudioToolbox</framework>
<framework>AVFoundation</framework>
</frameworks>
<!-- Classic backend: replace Metal with OpenGLES above -->
<!-- <framework>OpenGLES</framework> instead of <framework>Metal</framework> -->
<!-- Assets bundled with the app -->
<resources>
<resource>
<directory>../assets</directory>
</resource>
</resources>
<!-- CRITICAL: Force-link classes used via reflection -->
<forceLinkClasses>
<pattern>com.mygame.entities.**</pattern>
<pattern>com.mygame.data.SaveData</pattern>
</forceLinkClasses>
<!-- Info.plist entries -->
<iosInfoPList>
<dict>
<key>CFBundleDisplayName</key>
<string>My Game</string>
<key>UIStatusBarHidden</key>
<true/>
</dict>
</iosInfoPList>
</config>
robovm.properties
Simple key-value file for app metadata:
app.id=com.mygame.ios
app.name=MyGame
app.version=1.0
app.build=1
app.mainclass=com.mygame.ios.IOSLauncher
Reflection / forceLinkClasses (CRITICAL)
RoboVM strips classes it can't statically determine are used. If your game uses libGDX Json, ReflectionPool, or any reflection-based instantiation, those classes will be missing at runtime on iOS while working fine on desktop and Android.
Fix: Add <forceLinkClasses> patterns in robovm.xml:
<forceLinkClasses>
<pattern>com.mygame.entities.**</pattern> <!-- ** = all classes including subpackages -->
<pattern>com.mygame.items.*</pattern> <!-- * = classes in this package only -->
</forceLinkClasses>
Symptom: ClassNotFoundException or ReflectionException on iOS only. Game works on desktop/Android.
Build Commands (Gradle)
./gradlew ios:launchIPhoneSimulator # run on iPhone simulator
./gradlew ios:launchIOSDevice # run on connected physical device
./gradlew ios:createIPA # build IPA for distribution
First build is slow (AOT compilation). Subsequent builds use caching.
Lifecycle
| Event | Behavior |
|---|---|
| App goes to background | pause() called |
| App returns to foreground | resume() called |
| OpenGL context on pause | Preserved — textures NOT destroyed (unlike Android) |
| App terminated | dispose() called, but iOS rarely terminates cleanly — usually just suspends |
Key difference from Android: On iOS, the OpenGL context is preserved when the app backgrounds. You do NOT need to reload textures on resume. Android destroys the GL context on pause, requiring texture reloads — this does not happen on iOS.
dispose() is unreliable on iOS. iOS typically suspends apps rather than terminating them. Save critical state in pause(), not dispose().
File Access
| Method | iOS Location | Writable |
|---|---|---|
Gdx.files.internal() | App bundle (resources/) | No (read-only) |
Gdx.files.local() | App's Documents directory | Yes |
Gdx.files.external() | App's Documents directory | Yes |
local() and external() map to the same directory on iOS. There is no separate external storage concept — iOS apps are sandboxed. Unlike Android, there is no SD card, no shared storage, and no file permissions to request.
Platform Quirks
Safe Area / Notch (iPhone X+)
Use Gdx.graphics.getSafeInsetTop() etc. to get safe area insets for notch/Dynamic Island/home indicator:
float topInset = Gdx.graphics.getSafeInsetTop(); // notch / Dynamic Island
float bottomInset = Gdx.graphics.getSafeInsetBottom(); // home indicator bar
float leftInset = Gdx.graphics.getSafeInsetLeft(); // landscape notch
float rightInset = Gdx.graphics.getSafeInsetRight();
// Offset your UI: don't place buttons in the inset area
float safeTop = Gdx.graphics.getHeight() - topInset;
These return pixel values. Account for them when positioning UI elements near screen edges. Critical on all iPhone X and newer (notch and Dynamic Island).
Screen Density
float density = Gdx.graphics.getDensity();
// iPhone Retina: 2.0
// iPhone Plus / Pro Max: 3.0
Gdx.graphics.getWidth()/getHeight() return pixels (not iOS points). The density gives the Retina scale factor.
No Back Button
iOS has no system back button. Input.Keys.BACK is Android-only. You must provide in-app navigation (on-screen back buttons, swipe gestures). Do not use Gdx.input.setCatchKey(Input.Keys.BACK, true) on iOS — it has no effect.
On-Screen Keyboard
// Show/hide the soft keyboard directly
Gdx.input.setOnscreenKeyboardVisible(true); // show
Gdx.input.setOnscreenKeyboardVisible(false); // hide
// Or show a native text input dialog
Gdx.input.getTextInput(listener, "Title", "default text", "hint");
Gotcha: The soft keyboard covers the bottom portion of the screen. Your UI must account for this — move text fields up or resize the viewport. The native dialog (getTextInput) is modal and blocks game input.
Haptics
Gdx.input.vibrate() works on iOS only if useHaptics = true in IOSApplicationConfiguration. Without it, vibrate calls are silently ignored.
Gdx.input.vibrate(50); // duration in ms (iOS may round to standard haptic pattern)
Common Mistakes
- Assuming OpenGL context is lost on iOS background — That's Android. On iOS the GL context is preserved. No texture reloading needed on resume.
- Not using
<forceLinkClasses>for reflection — Classes only referenced via reflection (Json, ReflectionPool) are stripped by RoboVM AOT compilation. Add patterns in robovm.xml. Symptom:ClassNotFoundExceptionon iOS only. - Thinking
Gdx.files.external()differs fromGdx.files.local()on iOS — They both map to the Documents directory. No separate external storage on iOS. - Using
Gdx.input.setCatchKey(Input.Keys.BACK, true)on iOS — iOS has no back button. This is Android-only. Provide in-app navigation instead. - Not accounting for safe area insets — On iPhone X+ the notch/Dynamic Island and home indicator overlap the screen. Use
Gdx.graphics.getSafeInsetTop()etc. to offset UI. - Expecting
dispose()to be called — iOS typically suspends apps, not terminates them. Save state inpause(). - Using wrong Gradle task names — The correct tasks are
ios:launchIPhoneSimulator,ios:launchIOSDevice,ios:createIPA. Notios:launchIOSSimulatororios:launchIPhoneDevice. - Forgetting
useHaptics = truein config —Gdx.input.vibrate()silently does nothing on iOS without this config option. - Using JVM-specific features —
Runtime.exec(),ProcessBuilder, dynamic class loading with runtime-determined names all fail on iOS. RoboVM compiles to native ARM — there is no JVM. - Not knowing about
Gdx.graphics.getSafeInsetTop()— This is a libGDX API. Do not write custom RoboVM/UIKit code to get safe area insets — libGDX provides it cross-platform. - Using the classic
ios-robovmbackend for new projects without considering MetalANGLE — Apple deprecated OpenGL ES in iOS 12. The MetalANGLE backend (gdx-backend-robovm-metalangle) is recommended for forward-compatibility. Same libGDX API, different backend dependency only. - Including
OpenGLESframework in robovm.xml when using the MetalANGLE backend — MetalANGLE usesMetalframework instead. Using the wrong framework set causes black screen or crash on launch. - Running the classic
ios-robovmbackend on an arm64 iOS simulator (Apple Silicon Mac) — The classic backend doesn't support arm64 simulators. Use an x86_64 simulator (Rosetta) or switch to the MetalANGLE backend.