Dynamic Coding with Lua on iOS

Or, how I learned to stop worrying and love interpreted languages

For all iOS developers, Apple is an important part of the process. They act as the gatekeepers of quality.. or at least, that's the theory. In practice, however, after an app makes it through the gate the first time, all it seems to accomplish is slowing down the release process. Having a 5-10 day pause in the player feedback loop is crippling to a rapidly developing game that has to react to the demands of our increasingly minute-to-minute society.

The solution is to develop a system that allows Apple to remain happy while removing developer barriers. My day job focuses on games that use javascript and the UIWebView runtime to allow for downloadable and embedded javascript. However, there's issues with that. Largely, the javascript runtime is a slow disgusting beast. With the release of Codea, I've found a solution. Codea uses a custom engine, is fairly decently performant, and allows development on-device using Lua and an integrated editor. Sounds like a fun toy, right? It is! However, Codea also allows for export to an Xcode project, and apps may be released with it, royalty free.

All of a sudden, I've got a system that allows me to rapidly develop good performant code on-device, with on-screen parametric editors and export for distribution to a testing group, and subsequently to Apple for release.

There's still that missing piece, though. The ability to update on-demand. Codea provides a mechanism to download files from the web. Lua provides a mechanism to interpret lua code. Put them together and magic happens.

There are, however, some flies in that ointment. Apple may get their knickers in a twist about this: They've gone on record about 'NO DOWNLOADABLE CODE'...  but since then, they've officially backed off from embedded scripts, allowing using of such engines as Unity or Unreal Engine, and Codea itself. Downloadable code via the UIWebView runtime has also been permitted, because it's interpreted using an engine that Apple itself has written. The agreement itself is clear on this matter:

3.3.2 An Application may not download or install executable code. Interpreted code may only be used in an Application if all scripts, code and interpreters are packaged in the Application and not downloaded. The only exception to the foregoing is scripts and code downloaded and run by Apple's built- in WebKit framework, provided that such scripts and code do not change the primary purpose of the Application by providing features or functionality that are inconsistent with the intended and advertised purpose of the Application as submitted to the App Store. 

To there you have it. Your app might get removed, or your dev account could get banned. However, it's absolutely fine if you want to use this functionality on a pre-release build, you just have to go back to an embedded model before submission to the App Store. Or if you somehow can get written approval from Apple.

So... brass tacks. How to do it? Proof-of-concept follows.

Prerequisites:

  • iPad.
  • Codea - it's an iPad-only app. It's super cheap, for what it can do.
  • If you want to publish, Xcode & and a Developer Account. Exporting and Publishing left as an exercise for the reader.

Driver bootstrap - paste this into Codea. Change the URL.

modules = {}

function gotDynamicModule(data, status, headers)
loadstring(data)()

    -- run all the modules
    for name,m in pairs(modules) do
        print("Setting up module: " .. name)
        m.setup()
    end
end

function setup()
    print("Running setup()")

    -- module registration preamble
    http.request("http://example.org/test.lua", gotDynamicModule, (function(errString)
        print("Got error: " .. errString)
    end))

    print("module completed")
end

function touched(touch)
    print("touch")
    for name,m in pairs(modules) do
        m.touched(touch)
    end

    if (touch.state == ENDED) then
        http.request("http://example.org/test.lua", gotDynamicModule)
    end
end

-- This function gets called once every frame
function draw()
    for name,m in pairs(modules) do
        m.draw()
    end
end

Downloadable module file - put the following onto a webserver at the URL you put in the above file.

print("LOADING TEST.LUA")

modules["UI"] = {
    name = "UI",
    version = 1,
    setup = function()
        print("UI.setup")
    end,
    touched = function(touch)
        print("UI.touch")

        if touch.state == ENDED then
                print("touched")
        end
    end,
    draw = function()
        -- print("UI.draw")

        -- This sets a dark background color 
        background(40, 40, 50)

        -- This sets the line thickness
        strokeWidth(5)

        -- Do your drawing here
        rectMode(CENTER)
        rect(WIDTH/2, HEIGHT/2, 200, 200)
    end
}