Porting libGDX games to iOS, for Apple dummies (part 2/2)


Update (14 September 2016): A month after I wrote this, RoboVM announced that they were winding down. I already had a (free) license, which is good until April 2017, but if you need a new one, you’re out of luck.

The thing that comes closest is MobiDevelop’s RoboVM fork, but that is a stopgap at best, because it will probably never support bitcode (which Apple is pushing for). The alternative is Intel’s Multi-OS Engine (MOE), and with the future uncertain, both are actively being supported by libGDX right now. I recommend you read this post by Mario and make up your own mind about what to use.

Regardless of what happens in the long run, MobiDevelop’s RoboVM fork seems to be working fine for many people so far. If you want to use MOE instead, most of this article still applies anyway.


This is the second half of this mini-series, a sort of tutorial on porting your libGDX game to iOS from scratch. In case you missed it, read part 1 here. In that part, we bought hardware, installed software, and got to the point of running our game on the iOS Simulator. But the story is far from over; today, we run it on a real device, do some polishing, and upload it to the App Store!

6. Run your game on a real device

In order to run any of your own programs on a real iOS device, you used to need an Apple Developer Program account at a price of $99 per year, but this was changed with Xcode 7. Much like on Android, you will also need to worry about keys for signing your apps. Apple’s App Distribution Quick Start documents take you through the process of setting all this up. I recommend you read through this and understand how the process works, before throwing libGDX and RoboVM into the mix. In summary, the steps are:

  • Create an Apple ID via appleid.apple.com if you don’t have one yet.
  • In Xcode, create and download an “iOS Development” signing identity. The “iOS Distribution” one is only needed once you want to start distributing, with or without Apple’s TestFlight beta testing service.
  • Create a provisioning profile for development. Basically, this is a matter of creating a dummy Xcode project with a bundle identifier that matches that of your libGDX project, then following Apple’s guide. Make sure to select your actual device from the toolbar at the top of the Xcode window. You won’t be able to find these provisioning profiles in the online Member Center until you actually sign up for the Developer Program, but they are stored online and Xcode can find them!

All set? Then just update the run configuration in IntelliJ to run your game on a real iOS device. Point it at your signing identity and provisioning profile (or leave both at Auto). Hit Run, and with some luck, the game should make its way onto your iDevice!

When you tap its icon though, you’ll get a message about the app being made by an Untrusted Developer. To remedy this, go to Settings, General, Device Management. Tap your developer account (signing identity); the app should appear, along with an option to trust it.

7. Prepare a production IPA

There are also some other bits and bobs I needed to do to get a working .IPA file (equivalent of Android’s .apk). First: icons. These reside in ios/data in a stock libGDX project, but at the time of writing are not the complete set for all iOS devices up to the iPhone 6 Plus and iPad Pro (see Apple’s icon matrix and icon FAQ). There are 20 (!) different sizes, but for a game you can get away with the following 8 (not counting the one for the App Store at 1024×1024):

export_icon  57 ios/data/Icon.png       # iPhone, iOS <= 6.1
export_icon 114 ios/data/Icon@2x.png    # iPhone, iOS <= 6.1, Retina
export_icon  72 ios/data/Icon-72.png    # iPad, iOS <= 6.1
export_icon 144 ios/data/Icon-72@2x.png # iPad, iOS <= 6.1, Retina
export_icon 120 ios/data/Icon-60@2x.png # iPhone, iOS > 6.1, Retina
export_icon 180 ios/data/Icon-60@3x.png # iPhone 6 Plus, iOS > 6.1, Retina
export_icon  76 ios/data/Icon-76.png    # iPad, iOS > 6.1
export_icon 152 ios/data/Icon-76@2x.png # iPad, iOS > 6.1, Retina

As you can see, if you target just recent iOS versions (which you can probably get away with, as it’s some 98% of the market as of March 2016), you can leave out a few. But since I’m autogenerating the icons with a shell script, might as well add broad support.

You also need to update ios/Info.plist.xml (whose purpose is similar to AndroidManifest.xml) to include the Icon-60 and Icon-76 variants:

    ...
    <key>CFBundleIcons</key>
    <dict>
        <key>CFBundlePrimaryIcon</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>Icon</string>
                <string>Icon-60</string> <!-- added -->
                <string>Icon-72</string>
                <string>Icon-76</string> <!-- added -->
            </array>
        </dict>
    </dict>
    ...

While you’re in there, you should also disable multitasking support. This is a new feature on iOS 9 that lets apps run side-by-side on the same screen, but it requires a launch file in .xib or .storyboard format that I don’t know how to create (tutorials welcome!). Multitasking is disabled as follows:

    ...
    <key>UIRequiresFullScreen</key>
    <true/>
    ...

Also, it’s nice to support upside-down mode; unless you did something funny in your libGDX code, this should just work out of the box:

    ...
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string> <!-- added -->
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    ...

Speaking of launch files, you want to update these as well. A “launch file” or “launch image” is a static image displayed by iOS while the app is being loaded, to make it feel more responsive. As per Apple’s guidelines, this image needs to match the first screen in the app as closely as possible, so have it look like your loading screen or splash screen. Note that it cannot be localised, so don’t include any text. I’m using another shell script to generate these from a base image; the relevant formats for today’s world (again, up to iPhone 6 Plus and iPad Pro) are:

export_launch_image 320x480 ios/data/Default.png # iPhone 1-3
export_launch_image 640x960 ios/data/Default@2x.png # iPhone 4s (@2x)
export_launch_image 750x1334 ios/data/Default-375w-667h@2x.png # iPhone 6 (@2x)
export_launch_image 1242x2208 ios/data/Default-414w-736h@3x.png # iPhone 6 Plus (@3x)
export_launch_image 640x1136 ios/data/Default-568h@2x.png # iPhone 5 (@2x)
export_launch_image 768x1004 ios/data/Default~ipad.png # iPad 2 and iPad mini (@1x)
export_launch_image 1536x2008 ios/data/Default@2x~ipad.png # iPad and iPad mini (@2x)
export_launch_image 2048x2732 ios/data/Default-1024w-1366h@2x~ipad.png # iPad Pro (@2x)

The magic file suffixes do the work here, so these don’t need to be registered explicitly in any XML file. The sizes for iPads don’t match those from Apple’s documentation, but do match those from the libGDX project setup; I guess this has to do with the status bar. I also added the iPad Pro size, not currently generated by libGDX.

That should be it for iOS-specific assets.

8. Create your IPA file

To actually make your game available in the App Store (or even in Apple’s TestFlight beta program), you do need to shell out the $99/year for the Developer Program. You may want to reserve a few days for setting this account up, because your address will be verified automatically, and if this fails, it requires some back-and-forth with customer service.

Once your account is set up, log in to developer.apple.com and create a signing identity for distribution. Then create a provisioning profile for distribution. Go into Xcode settings and download both of them to your hard drive (detailed instructions).

For whatever reason, I couldn’t get the RoboVM plugin in IntelliJ to accept these credentials, so I went with the command-line Gradle option. It gives you a more reproducible build anyway. This requires just a few lines in the root build.gradle (many thanks to jephron on the libGDX forums for figuring this out):

project(":ios") {
    ...
    robovm {
        // Find this value by running:
        // security find-identity -v -p codesigning
        iosSignIdentity = "0123456789ABCDEF0123456789ABCDEF01234567"

        // Find this value by reading the files in
        // ~/Library/MobileDevice/Provisioning Profiles/
        // and picking the base name of the one corresponding to your
        // production profile.
        iosProvisioningProfile = "01234567-890a-bcde-f012-34567890abcd"

        iosSkipSigning = false

        // Enable these to fix any "file unwritable" exceptions if they crop up.
        // Was not a problem for me.
        // stdoutFifo = null
        // stderrFifo = null
    }
}

To avoid OutOfMemoryException while building, edit gradle.properties to raise the value of -Xmx to something like 2 GB:

org.gradle.jvmargs=-Xms128m -Xmx2048m

Since June 1, 2015, all App Store uploads require 64-bit support. You can specify this on the Gradle command line, but it’s better to hardcode it so you can never forget. To enable 64-bit support, edit ios/robovm.xml to include the arm64 architecture:

  ...
  <os>ios</os>
  <arch>thumbv7</arch>
  <arch>arm64</arch>
  ...

Finally, edit robovm.properties to set app.executable to the desired output filename (the default is IOSLauncher but you probably want to use your game’s name instead). Then run ./gradlew ios:createIPA to create the .IPA file!

Note that this file can be pretty big; mine is nearly 50 MB for a small game with just a few MB of assets. This is in part because of the cross-platform support; the App Store will split it into platform-specific files to prevent people having to download lots of data that doesn’t apply to their platform. Even then, expect your game to be significantly larger than on Android, due to the inclusion of a JVM and core libraries. In my case, these are the sizes (reported in iTunes Connect under Activity, All Builds, click the build, click App Store File Sizes):

  • Universal: 38 MB download, 47 MB install (used on pre-iOS 9 devices)
  • 64-bits: 22 MB download, 28 MB install
  • 32-bits: 19 MB download, 23 MB install

It’s the price we pay for cross-platform development. This is for a game with just 3 MB of assets, so if yours is more asset-heavy, the relative overhead should be smaller.

9. Distribute your game

Before uploading your app to iTunes Connect, you need to tell iTunes Connect to expect it, i.e. “create an iTunes connect record” for it. I won’t repeat Apple’s documentation about this.

To upload your IPA file if it wasn’t built in Xcode, you need to install the glorified file uploader app called “Application Loader”. Point it at your IPA file. If you are missing any launch files or configuration options, this is the stage at which you’ll be told. If there are no such errors, the Application Loader proceeds with actually uploading the IPA. Because all the metadata is in the file, it will know the account, app, provisioning profile and version number to use during the upload. You get an email when the IPA has been processed and is ready to be attached to a version in iTunes Connect. From that point on, it’s just clicking around in web forms.

In related news, Mystery Game No. 1 is going to beta Real Soon Now™. If you’re curious what it’s going to be, why not sign up to the newsletter? Happy Apple’ing!