Lua for App Development

Lua native bridge

This document provides an overview of Bounces Lua native bridge, the software layer allowing you to transparently mix Lua and native code in your application. In this article, you will learn how to use native objects in your Lua code, how you can use C structs, enums, and most other types in Lua, and how easy it is to make your Lua objects visible (and callable) from the native code.

This article supposes that you already know Lua, or at least that you feel comfortable reading basic Lua code. If you are not, you can read the first article in this series, Get started with Lua - The Lua language, that gives a simple but reasonably detailed introduction to the Lua language.

You also need to be familiar with the Bounces object framework; if you have no idea of what this object framework is, please read the corresponding section in Get started with Lua - Bounces additions to Lua.

This article includes many code examples introduced by short explanation texts. Comments in the code bring explanations and additional information, so they are probably worth reading. All code examples are valid tested code that you can execute in Bounces, and you can use the debugger to get an in-depth understanding of their behavior if you want.

There are several ways to use this article: you can read it sequentially, or you can use it as a quick reference of Bounces native bridge, by jumping directly to a specific topic via the article's table of content (use the button on the left to display it if hidden).

Bounces native bridge

Bounces includes a C / Objective-C bridge that allows you to use transparently in Lua, C and Objective-C APIs defined by the target platform SDK, as well as APIs defined by the native code in your application. Swift classes and APIs are supported too, provided they are visible from Objective-C, (for details, see Apple document Using Swift with Cocoa and Objective-C).

The Bounces native bridge functionality is provided by software packages called Bindings Libraries. A Bindings Library contains native bridging information for a given target platform SDK or for the native code in an Xcode project. Internally, it includes Lua interface code describing the exposed native API, and binary libraries to be linked with the target application.

Using native objects in Lua

Native objects in Bounces are integrated in the Bounces object framework. So you basically handle native classes and instances just like you do with classes and instances created in Lua.

But native objects have their own specificities, and this section provides an overview of the main things that you'll need to know in order to use native objects effectively in Lua.

All code examples in this section use classes of the Foundation framework. Therefore you can test any of them directly in a Bounces Local Lua Context (press ⌘⇧N to create one), without having to create a separate application. Once you have pasted a code sample in the Lua editor in Bounces, you can run it using the debug toolbar or the Execute menu.

Native objects look like Lua objects

The global variable objc gives you access to native classes from Lua.

-- Native classes are accessed via the 'objc' global table
local NSURL = objc.NSURL  -- local variable NSURL is now a reference to the native class NSURL

print (NSURL)  --> Class NSURL

-- You create a new instance using a 'new' method, like 'newXyz', corresponding to init method 'initXyz'
local photosAppUrl = NSURL:newFileURLWithPath ("/Applications/Photos.app") -- create an instance using initFileURLWithPath internally

-- You can naturally call class methods
local bouncesDocUrl = NSURL:URLWithString ("https://bouncing.dev/en/documentation/")

-- Native objects are Lua values, so you can do basically anything with them
local urls = { photosAppUrl, bouncesDocUrl } -- store native instances in a Lua table

print (photosAppUrl)  --  pass a native instance to a Lua function
                      --> file:///Applications/Photos.app/

-- Native properties can be get or set using the classic Lua field syntax
print (photosAppUrl.scheme, photosAppUrl.host)      --> file    nil
print (bouncesDocUrl.scheme, bouncesDocUrl.host)  --> https    www.celedev.com

-- Calling an instance method is also straightforward
local photoAppResourceUrl = photosAppUrl:URLByAppendingPathComponent('Contents'):URLByAppendingPathComponent('Resources')
print (photoAppResourceUrl)  --> file:///Applications/Photos.app/Contents/Resources/

In native method names, '_' fills the gaps

Lua names and Objective-C (or Swift) method names do not follow the same pattern, so Bounces had to adopt a certain convention for expressing native method names in Lua:

Lua names of native methods are generated by replacing any ':' characters in the original Objective-C method selector (except the last one) with an underscore ('_'). For example, an ObjC method named indexOfObject:inRange: is translated into a Lua method named indexOfObject_inRange.

Native and Lua types work well together

When calling native methods or getting/setting native object properties, the native bridge automatically does the necessary conversions between Lua types and native types.

-- Conversions between Lua and native types are automatic, and you generally don't need to think about them
local array1 = objc.NSMutableArray:arrayWithObject (2.1)  -- converts 2.1 into a NSNumber and create an NSMutableArray containing it
array1:addObject ("lorem ipsum")                          -- converts "lorem ipsum" into a NSString and adds it to the array
array1:addObject (true)                                   -- converts true into the NSNumber @YES and adds it to the array

local a, b, c = array1[1], array1[2], array1[3]  -- converts objects in the array into the corresponding Lua types and assign them to local variables a, b, and c
                                                 -- notice that native object indexes start at 1, like in Lua tables

-- The actual type conversion depends of the expected native type
local transform = objc.NSAffineTransform:transform()  -- create an affine transform object, initialized to the identity matrix
transform:translateXBy_yBy(100, 25)  -- this method expects CGFloat parameters, so Lua numbers here are converted to GCFloats

-- Tables passed to a parameter specified as NSArray or NSDictionary are converted to the expected type
array1:addObjectsFromArray { transform, 100, 25, 'dolor', 'sit', 'amet' } -- the provided Lua table is converted to a NSArray, the expected parameter type of method `addObjectsFromArray`, before being passed to the method

-- On the other hand, if you pass a table parameter to a native method expecting a generic id/AnyObject, 
-- no conversion is made and the method receives a Lua table reference object of class `CIMLuaTable`
array1:addObject { 'consectetur', 'adipiscing', 'elit' } -- this Lua table is NOT converted, as the parameter to `addObject` is typed as an id/AnyObject
print (type (array1.lastObject))  --> table

-- In any case, you can force the conversion of a table to a NSArray or NSDictionary, by using convenience functions `objc.toArray` or `objc.toDictionary`
array1:addObject (objc.toArray { 'sed', 'do', 'eiusmod' })
print (type (array1.lastObject))  --> objcinstance

Strings can use native methods

Strings have access to methods of both Lua string library and native NSString class, which is quite a powerful mix:

local loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."

-- Using Lua strings library, invert any two successive words separated by spaces
local ipsumLorem = loremIpsum:gsub("(%w+)%s+(%w+)", "%2 %1")
print (ipsumLorem) --> ipsum Lorem sit dolor amet, adipiscing consectetur elit, do sed tempor eiusmod ut incididunt et labore magna dolore aliqua.

-- Using ObjC NSString methods, capitalize the previous string
local ipsumLoremCapitalized = ipsumLorem:capitalizedString()
print (ipsumLoremCapitalized) --> Ipsum Lorem Sit Dolor Amet, Adipiscing Consectetur Elit, Do Sed Tempor Eiusmod Ut Incididunt Et Labore Magna Dolore Aliqua.

Native blocks are functions

Swift closures / Objective-C blocks are equivalent to Lua functions:

-- Download an extract of the Wikipedia article about Lua

local wikipediaLuaUrl = objc.NSURL:URLWithString "https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=Lua_(programming_language)"
local urlSession = objc.NSURLSession.sharedSession -- notice the use of a native class property here

-- This function is the completion handler for the download task.
-- Here we store it in a local variable, for better readability, but we could also define it inline in the call to `downloadTaskWithURL_completionHandler`
local function downloadCompletionHandler (location, response, error)
    -- location is a temporary file URL where the reply is stored
    local replyDictionary = objc.NSJSONSerialization:JSONObjectWithData_options_error(objc.NSData:newWithContentsOfURL(location), 0)

    if replyDictionary then
        local firstPageInReply = replyDictionary.query.pages.allValues.firstObject -- 'query' and 'pages' are keys in the reply dictionary, 'allValues' is a NSDictionary property returning an array of values in the dictionary, 'firstObject' is a NSArray property

        if firstPageInReply then
            print (firstPageInReply.extract)
        end
    end
end

 -- Create a download task and pass function `downloadCompletionHandler` as the completion handler block parameter
local downloadTask = urlSession:downloadTaskWithURL_completionHandler (wikipediaLuaUrl, downloadCompletionHandler)
-- Start the download
downloadTask:resume()

--> Lua (/ˈluːə/ LOO-ə, from Portuguese: lua [ˈlu.(w)ɐ] meaning moon; explicitly not "LUA" because it is not an acronym) is a lightweight multi-paradigm programming language designed primarily for embedded systems and clients. Lua is cross-platform since it is written in ANSI C, and has a relatively simple C API.
--> Lua was originally designed in 1993 as a language for extending software applications to meet the increasing demand for customization at the time. It provided the basic facilities of most procedural programming languages, but more complicated or domain-specific features were not included; rather, it included mechanisms for extending the language, allowing programmers to implement such features. As Lua was intended to be a general embeddable extension language, the designers of Lua focused on improving its speed, portability, extensibility, and ease-of-use in development.

Out parameters are results

Output parameters in native methods are transformed into Lua function results, which is both logical and elegant:

-- Get the modification date of the file at photosAppUrl

local NSURL = objc.NSURL  -- local variable NSURL is now a reference to NSURL native class
local photosAppUrl = NSURL:newFileURLWithPath ("/Applications/Photos.app") -- create an instance using initFileURLWithPath internally

-- Enum values related to a native SDK class are stored as fields of this class; e.g. enum NSURLResourceKey is accessible as NSURL.ResourceKey
local key = NSURL.ResourceKey

-- Output parameters of native methods become method results in Lua (including NSError**/NSErrorPointer parameters)

local isOk, modificationDate, error = photosAppUrl:getResourceValue_forKey_error(key.contentModificationDate)

if isOk then
    print ("Photos app modif date: ", modificationDate) --> Photos app modif date:  2016-03-22 08:37:22 +0000
else
    print ("Cannot get Photos app modif date: ", error)
end

-- Similarly blocks output parameter (like ObjC `BOOL* stop` or Swift `UnsafeMutablePointer<ObjCBool>`) become results of the block function
local photoAppContentsUrl = photosAppUrl:URLByAppendingPathComponent('Contents')
local fileManager = objc.NSFileManager.defaultManager
local photosAppContentsItems = fileManager:contentsOfDirectoryAtURL_includingPropertiesForKeys_options_error (photoAppContentsUrl, {}, 0)

photosAppContentsItems:enumerateObjectsUsingBlock (function (childUrl, index)
                                                       print (childUrl.lastPathComponent) 
                                                       if index == 5 then
                                                           print "stopping enumeration"
                                                           return true -- stop the enumeration
                                                       end
                                                   end)
    --> _CodeSignature    Info.plist    Library    MacOS    PkgInfo    Resources
    --> stopping enumeration

Native collections behave like tables

Native collections have consistent table-like behaviors. So you can use the exact same syntax and operators, whether you use tables or native collection objects in your Lua code.

-- Read the contents of the 'Photos' application bundle
local photoAppContentsUrl = objc.NSURL:newFileURLWithPath ("/Applications/Photos.app/Contents/")
local fileManager = objc.NSFileManager.defaultManager
local URLKey = objc.NSURL.ResourceKey -- URL-related enums are exposed as class fields

-- The contents of a directory is returned as an array of NSURLs
local photosAppContentsItems = fileManager:contentsOfDirectoryAtURL_includingPropertiesForKeys_options_error (photoAppContentsUrl, {URLKey.creationDate}, 0)

-- Use the # operator to get the collection's elements count
print (#photosAppContentsItems) --> 8
-- use indexing to get a collection element (note that array indexes start at 1)
print (photosAppContentsItems [1]) --> file:///Applications/Photos.app/Contents/_CodeSignature/

-- create a native dictionary for storing contents items creation dates
local contentsItemCreationDates = objc.NSMutableDictionary:new()

-- enumerate array elements using Lua for-in loops
for childUrl in photosAppContentsItems do  -- no need to use ipairs here because native collection instances can be passed as generators to for-in loops
    local childName = childUrl.lastPathComponent
    print (childName) --> _CodeSignature    Info.plist    Library    MacOS    PkgInfo    Resources    version.plist    XPCServices

    -- get the child creation date and adds it to the contentsItemCreationDates dictionary
    local hasDate, creationDate = childUrl:getResourceValue_forKey_error(URLKey.creationDate)
    if hasDate then
        contentsItemCreationDates [childName] = creationDate -- set a NSDictionary element using the indexed notation
    end
end

-- Use the contentsItemCreationDates dictionary
print (contentsItemCreationDates["PkgInfo"])  --> 2015-08-24 06:25:27 +0000   (indexed notation)
print (contentsItemCreationDates.XPCServices) --> 2015-08-24 06:25:32 +0000   (field notaion)

for name, date in contentsItemCreationDates do -- enumerate the dictionary keys and values
    print(name, date) --> version.plist     2016-02-14 07:54:31 +0000
    --> _CodeSignature    2015-08-24 06:25:51 +0000
    --> ...
end

Using Lua objects from native code

In the previous section, we have discussed how Bounces native bridge makes native objects and methods available to your Lua code. But you couldn't write an application in Lua if the bridge wasn't also working in the other direction: making your Lua objects and methods visible (and callable) from the native world is mandatory for using common design patterns like delegation, target-action or subclass-to-customize.

This section will present the main mechanisms provided by Bounces for exposing Lua objects to native code: methods publishing, native class subclassing, and native class extensions.

Some examples in this section are iOS-specific, and so can not be run in a Bounces local Lua context window. If you want to try them anyway, you can create an empty iOS application project in Bounces, and integrate the code samples in this application.

Only methods with known interfaces are published

In Swft or C / Objective-C native code, a function can only be called if the specific type of each of its parameters is known at compile time. In Lua, on the other hand, function parameters are not typed, and a parameter can receive a value of any type when the function is called.

For this reason, methods and properties defined in a Lua class are by default not visible from the native code. For a Lua method to be called by native code, this method must declare a native method interface. And actually, any Lua object method for which a native interface is known, can be called from native code, just as if it were a regular Swift or Objective-C method.

Lua classes can adopt objc protocols

There are a few different ways to declare a native interface for a Lua method. The simplest and most flexible one is to use an Objective-C protocol. If you are not familiar with Swift or Objective-C, a protocol is a group of method and properties definitions, that a class can implement for a given purpose, similar to an interface in Java or C#.

By declaring that it conforms to a given protocol, a Lua class exposes its own implementation of the protocol's methods and properties to the native world. This declaration is done by calling a Lua class method named declareProtocol, as shown in the next code sample.

Don't be afraid of this rather long example! It is actually very simple. The important points in it are the call to JsonUrlLoader:declareProtocol ('NSURLSessionDownloadDelegate') and the 3 methods of this protocol implemented by the class, that are not in any way different from other methods.

-- A Lua class that loads Json files using ObjC NSURLSession and acting as the delegate of its NSURLSession

local JsonUrlLoader = class.create ("JsonUrlLoader")

-- Instance initializer
function JsonUrlLoader:init() 
    -- create an URLSession and pass self (a Lua instance!) as the delegate
    local urlSessionConfiguration = objc.NSURLSessionConfiguration.defaultSessionConfiguration
    self.urlSession = objc.NSURLSession:sessionWithConfiguration_delegate_delegateQueue (urlSessionConfiguration, self, objc.NSOperationQueue.mainQueue)

    -- create a table of completion handlers, so several json downloads can be done in parallel
    self.completionHandlers = {} -- a table [downloadTask] --> completionHandler
end

-- Main method called by a client to download a JSON URL 
function JsonUrlLoader:getJsonAtURL_WithCompletionHandler (url, completionHandler --[[@type function(jsonObject, error)]]) -- the @type comment is only there for documenting the completionHandler parameter
    -- Create a download task
    local downloadTask = self.urlSession:downloadTaskWithURL (url)
    -- Save the completion handler for this task
    self.completionHandlers [downloadTask] = completionHandler -- remember, a Lua table key can be any value except nil, so it is ok to use a native object as a key
    -- Start the download
    downloadTask:resume()
end

-----------------------------------------------------------
-- Declare that class JsonUrlLoader adopts the 'NSURLSessionDownloadDelegate' protocol
JsonUrlLoader:declareProtocol ('NSURLSessionDownloadDelegate')

-- Implement methods declared in this protocol. These 3 methods will be callable from native code

function JsonUrlLoader:URLSession_task_didCompleteWithError (session, task, error)
    -- If an error occurred, call the task's completion handler, else URLSession_downloadTask_didFinishDownloadingToURL will have handled it
    if error then
        self:callCompletionHandler (task, nil, error)
    end        
end

function JsonUrlLoader:URLSession_downloadTask_didFinishDownloadingToURL (session, downloadTask, location)
    -- location is a temporary file URL where the reply is stored
    if location then
        -- convert the downloaded file to a JSON object
        local jsonObject, jsonError = objc.NSJSONSerialization:JSONObjectWithData_options_error(objc.NSData:newWithContentsOfURL(location), 0)
        -- call the completion handler
        self:callCompletionHandler (downloadTask, jsonObject, jsonError)
    end
end

function JsonUrlLoader:URLSession_downloadTask_didWriteData_totalBytesWritten_totalBytesExpectedToWrite (session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
    -- Print a progress message
    print (string.format("Downloaded %d Bytes (of %d) from %s", 
                         totalBytesWritten, totalBytesExpectedToWrite, downloadTask.currentRequest.URL.host))
end
-----------------------------------------------------------

-- internal method
function JsonUrlLoader:callCompletionHandler (downloadTask, ...)
    -- Call the registered completion handler for the specified download task 
    local taskCompletionHandler = self.completionHandlers [downloadTask]
    if taskCompletionHandler then
        -- call the completion handler with the provided additional parameters
        taskCompletionHandler (...)
        -- remove it from the completion handlers table
        self.completionHandlers [downloadTask] = nil
    end
end

return JsonUrlLoader
-- Use the JsonUrlLoader to get the text of the wikipedia page about Lua
local wikipediaLuaUrl = objc.NSURL:URLWithString "https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&explaintext=&titles=Lua_(programming_language)"

local JsonUrlLoader = require "JsonUrlLoader" -- Load the JsonLoader class

-- Create a Json URL loader
local jsonLoader = JsonUrlLoader:new()

-- Load the wikipedia page text about Lua
jsonLoader:getJsonAtURL_WithCompletionHandler (wikipediaLuaUrl, 
             function(jsonObject, error)
                 if jsonObject then
                     local firstPageInReply = jsonObject.query.pages.allValues.firstObject -- 'query' and 'pages' are keys in the reply dectionary, 'allValues' is a NSDictionary property returning an array of values in the dictionary, 'firstObject' is a NSArray property

                     if firstPageInReply then
                         print (firstPageInReply.extract)
                     end
                 elseif error then
                     print ("Error", error.localizedDescription)
                 end
             end)

--> Downloaded 6860 Bytes (of -1) from en.wikipedia.org
--> Downloaded 25224 Bytes (of -1) from en.wikipedia.org

--> Lua (/ˈluːə/ LOO-ə, from Portuguese: lua [ˈlu.(w)ɐ] meaning moon; explicitly not "LUA" because it is not an acronym) is a lightweight multi-paradigm programming language designed primarily for embedded systems and clients. Lua is cross-platform since it is written in ANSI C, and has a relatively simple C API.
--> Lua was originally designed in 1993 as a language for extending software applications to meet the increasing demand for customization at the time. It provided the basic facilities of most procedural programming languages, but more complicated or domain-specific features were not included; rather, it included mechanisms for extending the language, allowing programmers to implement such features. As Lua was intended to be a general embeddable extension language, the designers of Lua focused on improving its speed, portability, extensibility, and ease-of-use in development.
--> ...

Lua classes can declare action methods

The target-action design pattern is widely used in iOS and MacOS. The idea behind target-action is simple: a user interface component is dynamically connected to a target object that implements an action method, and when the user interacts with it in a certain way (for example a press on a button), the corresponding action method of the target object is called.

You can define action methods in a Lua class. You just need to declare them, using the class method declareActionMethod, to make them visible from the native code.

local GestureController = class.create ("GestureController")

-- Define an action method
function GestureController:handleSwipeGesture (recognizer --[[@type objc.UISwipeGestureRecognizer]])
    -- Handle the swipe gesture
    local swipeDirection = recognizer.direction -- the direction of the swipe (right, left, up or down)
    local location = recognizer:locationInView(self.view) -- the location of the gesture inside the current view
    -- ...
end

-- Declare handleSwipeGesture as an action method, to make it callable from native code
GestureController:declareActionMethod ('handleSwipeGesture') -- the parameter is the method name

-- define a view property for this class
GestureController.view --[[@type objc.UIView]] = property()

function GestureController:SetView (aView) -- setter for the view property
    -- Update the internal view storage
    self._view = aView

    -- Create the gesture recognizer using self as the target and 'handleSwipeGesture' as the action
    local swipeRecognizer= objc.UISwipeGestureRecognizer:newWithTarget_action (self, 'handleSwipeGesture') -- in the action parameter you pass the method name (a string)
    -- Add the gesture recognizer to the view
    self._view:addGestureRecognizer (swipeRecognizer)
end

-- ...

return GestureController

Note that @type comments are used in the above code. They are just comments that have no impact on code execution. What they do is to help the code editor to suggest correct completions when you type the code, in cases where the class/type of a variable just can't be guessed by analyzing the code!

Native classes can be subclassed in Lua

Not surprisingly, the Bounces object framework can create subclasses of a native class, or said differently, you can define in Lua a class inheriting from a native Objective-C or Swift class. In practice, you create a subclass of a native class exactly like you create a subclass of a Lua class, by calling method createSubclass on the superclass, or by passing the superclass as the second parameter of function class.create.

-- Create a custom view class named "MyViewClass"
local MyViewClass = objc.UIView:createSubclass ("MyViewClass") -- or class.create ("MyViewClass", objc.UIView)

-- Implement custom drawing code
function MyViewClass:drawRect (rect)
    -- Custom view drawing code
    -- ...
end

return MyViewClass

You can override methods of the native superclass, like drawRect() in the above example, and these overridden methods are automatically published to the native code. Additional methods in the class can be published if needed, as part of a protocol, or as action methods.

To illustrate this, here is complete runnable example that defines a subclass of iOS UIViewController. This view controller class overrides methods loadView() and viewDidLoad() to create and configure a UITableView. To control the TableView, our ViewController class adopts two protocols: UITableViewDataSource and UITableViewDelegate, and it implements a few basic methods of these protocols.

-- Create a View Controller class by subclassing native class UIViewController
local ViewController = objc.UIViewController:createSubclass ("ViewController")

local superclass = objc.UIViewController -- a convenience variable for calling superclass methods

-- Methods overriding native methods of the superclass

function ViewController:loadView ()
    -- Here the controller's view is created programmatically.    
    self.view = objc.UITableView:new()
end  

function ViewController:viewDidLoad ()
    self[superclass]:viewDidLoad() -- call the viewDidLoad method of the superclass

    self:configureView () -- configure the controller's view
end

local kCellIdentifier = "simple-cell" -- A local variable defining a shared constant string

-- Internal method (visible only from Lua code)

function ViewController:configureView ()
    local tableView = self.view 
    tableView.dataSource = self  -- the view controller is the tableView's data source
    tableView.delegate = self     -- the view controller is the tableView's delegate
    -- register a cell class for the tableView
    tableView:registerClass_forCellReuseIdentifier (objc.UITableViewCell, kCellIdentifier)
end

-- Declare several protocols by grouping them in a table
ViewController:declareProtocol { 'UITableViewDataSource', 'UITableViewDelegate' }

-- Methods defined in protocol UITableViewDataSource 

local sampleDataModel = { "Lorem ipsum", "dolor sit amet", "consectetur adipiscing elit", "sed do eiusmod tempor", "incididunt ut labore",  "et dolore magna aliqua" }

function ViewController:tableView_numberOfRowsInSection (tableView --[[@type objc.UITableView]], section --[[@type integer]])
    return #sampleDataModel
end

function ViewController:tableView_cellForRowAtIndexPath (tableView --[[@type objc.UITableView]], indexPath --[[@type objc.NSIndexPath]])
    local cell = tableView:dequeueReusableCellWithIdentifier_forIndexPath (kCellIdentifier, indexPath)

    cell.textLabel.text = sampleDataModel [indexPath.row + 1] -- +1 because Lua table indexes start at 1

    return cell
end

-- Methods defined in protocol UITableViewDelegate 

function ViewController:tableView_didSelectRowAtIndexPath (tableView --[[@type objc.UITableView]], indexPath --[[@type objc.NSIndexPath]])
    -- Print a message in the Lua console when the user selects a table cell
    print ("selected table row:", indexPath.row + 1)
end

-- return the ViewController class
return ViewController

Here is what you see when running this example in the iPhone simulator. You can select a row in in the table view and see the corresponding message "selected table row: n" printed in the Lua console.

The result of our ViewController

Native classes can have Lua extensions

Subclassing native classes in Lua is not always the most effective way of using Lua for application development. For example, Interface Builder in Xcode only knows about classes, outlets and actions declared in the curent Xcode project, so a storyboard can not reference a ViewController class defined in Lua. But don't worry, this is where class extensions come to the rescue!

Class extensions are a convenient way to split the definition of a Lua class into several modules, but you can also use a class extension to extend a native Swift or Objective-C class in Lua.

When you extend a native class in Lua, you get the best of both worlds: the extended class combines the flexibility of the Bounces object framework with the possibility to be used in Xcode storyboards, or to implement part of its methods in Swift or Objective-C.

In addition, when extending a native class, you have the possibility to override native methods of the class in Lua, i.e. to replace the original native implementation of a method by the one you provide in Lua. You can even call the native implementation of an overridden method using the notation self[objc]:method(), as you can see in the next example.

To illustrate this, let's rewrite the ViewController above using a class extension. Here, the ViewController class has been declared in Swift; table-view settings and configuration of the view controller as the table-view's data source and delegate, have been done in a storyboard.

-- ViewController is a class extension of the native 'ViewController' class defined in the associated Xcode project
-- (subclass of UIViewController, adopting protocols UITableViewDelegate and UITableViewDataSource)

local ViewController = objc.ViewController:addClassExtension() -- This creates a Lua class extension on the native 'ViewController' class

-- Redefine method viewDidLoad, already implemented in Swift
function ViewController:viewDidLoad()
    self[objc]:viewDidLoad() -- call the native version of the 'viewDidLoad' method

    self.tableView.allowsMultipleSelection = true -- property 'tableView' is declared in Swift and set in a storyboard
 end

-- Implement methods defined in protocol UITableViewDataSource 

local kCellIdentifier = "simple-cell" -- A local variable defining a shared constant string

local sampleDataModel = { "Lorem ipsum", "dolor sit amet", "consectetur adipiscing elit", "sed do eiusmod tempor", "incididunt ut labore",  "et dolore magna aliqua" }

function ViewController:tableView_numberOfRowsInSection (tableView --[[@type objc.UITableView]], section --[[@type integer]])
    return #sampleDataModel
end

function ViewController:tableView_cellForRowAtIndexPath (tableView --[[@type objc.UITableView]], indexPath --[[@type objc.NSIndexPath]])
    local cell = tableView:dequeueReusableCellWithIdentifier_forIndexPath (kCellIdentifier, indexPath)
    cell.textLabel.text = sampleDataModel [indexPath.row + 1] -- +1 because Lua table indexes start at 1
    return cell
end

-- Implement methods defined in protocol UITableViewDelegate 

function ViewController:tableView_didSelectRowAtIndexPath (tableView --[[@type objc.UITableView]], indexPath --[[@type objc.NSIndexPath]])
    print ("selected table row:", indexPath.row + 1)
end

return ViewController

In summary, here the declaration and configuration of the ViewController are done in Xcode, and its behavior is implemented in Lua.

Using C entities in Lua

When developing an application, you generally need to use C entities: struct types, enum parameters, string constants, and C functions are widely used in Apple's SDKs, and of course, the Bounces native bridge exposes them to your Lua code.

Structs are table-like objects

C structs in Bounces are exposed as lightweight objects.

-- You get a struct type via the 'struct' global table
local CGPoint = struct.CGPoint -- variable CGPoint is now a reference to the CGPoint struct type

-- You create struct objects by calling the struct type as a constructor
local point1 = CGPoint (100, 25) -- in this first variant, you provides the struct fields as parameters, in the order in which they are defined
print (point1)  --> <CGPoint 0x60800026d130> { x= 100, y= 25 }

local point2 = CGPoint {x = 42, y = 84} -- in this second variant, you pass a table as the parameter. Field names must match the struct's field names!

-- Struct fields are accessed like table field
point2.y = point1.y  -- You can get and set individual field structs

-- Structs have methods!

-- some methods are predefined in bindings libraries, like GCPoint:equalToPoint()
print (point1:equalToPoint(point2)) --> false
point1.x = 42
print (point1:equalToPoint(point2)) --> true

-- And you can define your own struct methods if you want
function CGPoint:moveByOffset (dx, dy)
    self.x = self.x + (dx or 0)
    self.y = self.y + (dy or 0)
end

-- Call your custom method
point1:moveByOffset(-10, 384)  -- point1: <CGPoint 0x60800026d130> { x= 32, y= 409 }

-- Struct assignment work by reference, like for all Lua table/object values

local point3 = point1
print (point3)  -->  <CGPoint 0x60800026d130> { x= 32, y= 409 }  (point3 and point1 are references to the same struct object)
point3.x = 0
print (point1)  -->  <CGPoint 0x60800026d130> { x= 0, y= 409 }   (point1 is changed, because point1 and point3 are references to the same struct object)

-- If assign-by-reference is not what you want, you use the 'copy' method to duplicate a struct
point3 = point1:copy()
print (point3)  -->  <CGPoint 0x6180002686b0> { x= 0, y= 409 }   (point3 and point1 are references to different struct objects)
point3.x = 222  -- point3: <CGPoint 0x6180002686b0> { x= 222, y= 409 }
print (point1)  -->  <CGPoint 0x60800026d130> { x= 0, y= 409 }   (point1 is not changed, as point1 and point3 are references to different struct objects)

Some structs have fields that are also structs:

-- Struct can be composed of structs
local rect = struct.CGRect { origin = point1, size = {width = 1920, height = 1080 }}
print (rect)  -->  <CGRect 0x610000099ee0> { origin= { x= 0, y= 409 }, size= { width= 1920, height= 1080 } }

-- getting and setting sub-fields  behave as expected
rect.size.height = 1200
print (rect)  --> <CGRect 0x610000099ee0> { origin= { x= 0, y= 409 }, size= { width= 1920, height= 1200 } }

-- calling struct method on struct fields also produce the expected result
rect.origin:moveByOffset (100, 120)
print (rect)  --> <CGRect 0x610000099ee0> { origin= { x= 100, y= 529 }, size= { width= 1920, height= 1200 } }

-- keep in mind that assignments are made by reference!
local someSize = rect.size
someSize.width = 1280   -- exactly the same as: rect.size.width = 1280, so it changes the width of rect
print (rect)  --> <CGRect 0x610000099ee0> { origin= { x= 100, y= 529 }, size= { width= 1280, height= 1200 } }

And because struct constructors accept table parameters, you can pass a table in place of a struct in a function, method or property:

-- In the context of an iOS View Controller controlling a table-view
local tableView = self.tableView

-- Set the center of the tableView (CGPoint property)
tableView.center = { x = 150, y = 200 }

-- Scroll the tableView to a certain offset (first parameter expects a CGPoint)
tableView:setContentOffset_animated ({ x = 0, y = 500 }, true)

Enums, constants and functions are in binding modules

Lua interface for native C enum types, global variables and functions (called C entities in this section) are provided in the Bindings Library generated for their definition SDK, more specifically in the Lua interface module corresponding to their declaration header file.

From Lua, you access C entities by reading the corresponding Lua interface module with the require function. For example, if you write local UIApplication = require 'UIKit.UIApplication', variable UIApplication will give access to every enum or external function related to this module, by using simple field indexing like UIApplication.State.background.

Alternatively, for object-oriented SDK modules, C entities can be accessed as fields of the module-defined class, removing the need to call require. For example, the UIApplication background state enum value can be directly accessed as objc.UIApplication.State.background.

Let's illustate this with a complete example. If we consider the UIKit.UIDevice Lua interface module in the iOS bindings Library, enum types and external functions declared in this module looks like this:

-- ...

-- Enum definition: UIDeviceOrientation
local UIDeviceOrientation --[[@typedef enum.UIDeviceOrientation; @inherits integer]]
                          = { unknown = 0,
                              portrait = 1,
                              portraitUpsideDown = 2,
                              landscapeLeft = 3,
                              landscapeRight = 4,
                              faceUp = 5,
                              faceDown = 6,
                            }

-- Enum definition: UIDeviceBatteryState
local UIDeviceBatteryState --[[@typedef enum.UIDeviceBatteryState; @inherits integer]]
                           = { unknown = 0,
                               unplugged = 1,
                               charging = 2,
                               full = 3,
                             }

-- Enum definition: UIUserInterfaceIdiom
local UIUserInterfaceIdiom --[[@typedef enum.UIUserInterfaceIdiom; @inherits integer]]
                           = { unspecified = -1,
                               phone = 0,
                               pad = 1,
                               tV = 2,
                               carPlay = 3,
                             }

-- Enum definition: NSNotificationName
local NSNotificationName --[[@typedef enum.NSNotificationName; @inherits string]]
                         = { orientationDidChange = "" --[[ UIDeviceOrientationDidChangeNotification ]],
                             batteryStateDidChange = "" --[[ UIDeviceBatteryStateDidChangeNotification ]],
                             batteryLevelDidChange = "" --[[ UIDeviceBatteryLevelDidChangeNotification ]],
                             proximityStateDidChange = "" --[[ UIDeviceProximityStateDidChangeNotification ]],
                           }


-- Class extension: UIDevice (EnumsAndConstants)
local UIDevice --[[@typedef objc.UIDevice; @category EnumsAndConstants]] = {}

UIDevice.orientationIsPortrait = function (orientation --[[@type enum.UIDeviceOrientation]]) --[[@return bool]] end
UIDevice.orientationIsLandscape = function (orientation --[[@type enum.UIDeviceOrientation]]) --[[@return bool]] end
UIDevice.orientationIsFlat = function (orientation --[[@type enum.UIDeviceOrientation]]) --[[@return bool]] end
UIDevice.orientationIsValidInterfaceOrientation = function (orientation --[[@type enum.UIDeviceOrientation]]) --[[@return bool]] end
UIDevice.Orientation = UIDeviceOrientation
UIDevice.BatteryState = UIDeviceBatteryState
UIDevice.UserInterfaceIdiom = UIUserInterfaceIdiom
UIDevice.Notification = NSNotificationName

return UIDevice

We can see that the UIDevice Lua interface module includes the definition of 4 enums types (UIDeviceOrientation, UIDeviceBatteryState, UIUserInterfaceIdiom and NSNotificationName) and 4 external functions (UIDevice.orientationIsPortrait and UIDevice.orientationIsLandscape, UIDevice.orientationIsFlat and UIDevice.orientationIsValidInterfaceOrientation). The enum types are stored as fields of the UIDevice class (UIDevice.Orientation…)

With this Lua interface, writing a View Controller using UIDevice enum types is easy:

-- This module defines a class extension of a native class 'DeviceInfoViewController'
-- The view of a DeviceInfoViewController contains a UILabels for displaying the battery state and a progress view for the battery level

local DeviceInfoViewController = objc.DeviceInfoViewController:addClassExtension()

local UIDevice = objc.UIDevice
local currentDevice = UIDevice.currentDevice

function DeviceInfoViewController:viewDidLoad ()
    self[objc]: viewDidLoad() -- call the native viewDidLoad method if any

    currentDevice.batteryMonitoringEnabled = true -- Enable battery monitoring
    self:updateBatteryInformation() -- Update the battery information

    -- Monitor battery-related notifications. Notification names are found in the UIDevice class
    local notificationCenter = objc.NSNotificationCenter.defaultCenter
    notificationCenter:addObserver_selector_name_object (self, "updateBatteryInformation", 
                                                         UIDevice.Notification.batteryStateDidChange, nil)
    notificationCenter:addObserver_selector_name_object (self, "updateBatteryInformation", 
                                                         UIDevice.Notification.batteryLevelDidChange, nil)
end

-- Define a table that converts battery state enum values into strings to be used in the battery state label
local State = UIDevice.BatteryState -- put the BatteryState enum in a local variable
local BatteryStateStrings = { [State.unplugged] = "Unplugged",
                              [State.charging]  = "Charging",
                              [State.full]      = "Battery full!"
                            }

function DeviceInfoViewController:updateBatteryInformation(notification)
    -- update the UI components for the current battery state
    self.batteryLevelIndicator.progress = currentDevice.batteryLevel
    self.batteryLevelLabel.text = string.format ("%s - %d%%", 
                                                 BatteryStateStrings [currentDevice.batteryState] or "?",
                                                 currentDevice.batteryLevel * 100)
end

-- declare 'updateBatteryInformation' as an action method(a notification handler has the same interface as a single-parameter action method)
DeviceInfoViewController:declareActionMethod("updateBatteryInformation")

return DeviceInfoViewController

If you run this example, it displays the device battery status like this:

The device info ViewController in action

Notice how elements of the bindings module table can have short Swift-like names when Cocoa naming conventions are respected: for example, the C enum value UIDeviceBatteryStateUnplugged is presented a a table field UIDevice.BatteryState.unplugged, and if the battery state enum is stored in a local variable BatteryState, the enum value can be written as BatteryState.unplugged.

Other C types are opaque Lua values

When you write Lua code using C APIs, you often have to deal with value types that have no Lua equivalent, but that are nevertheless required to use the API. For example, the CoreGraphics framework in iOS makes a heavy usage of "ref" types (CGContextRef, CGColorRef…); "ref" types are actually pointer types and have no direct equivalent in Lua or in the Bounces object framework. However you can still use them in your Lua code.

The Bounces native bridge treats values of unknown C types as opaque values. When you get an opaque C value as the result of a function or method, you can store it in a variable, and you can pass it later as a parameter to a function or method call. And actually, this is all you need for dealing with opaque C types in your Lua code.

To illustrate this, here is a code sample using the CoreGraphics framework:

-- This module defines a class extension of 'GradientView', an empty UIView subclass defined in Swift

-- Loads needed SDK bindings modules
local UiGraphics = require 'UIKit.UIGraphics'
local CgContext = require 'CoreGraphics.CGContext'
local CgGradient = require 'CoreGraphics.CGGradient'
local CgColorSpace = require 'CoreGraphics.CGColorSpace'

local GradientView = objc.GradientView:addClassExtension()

-- An internal function that creates a CGGradient
local function createGradientInRGBSpaceWithColorComponents (gradientColorComponents, locations)
    local colorSpace = CgColorSpace.createDeviceRGB()  -- colorSpace contains an opaque CGColorSpaceRef value
    local gradient = CgGradient.createWithColorComponents(colorSpace, gradientColorComponents, locations, #locations)
    CgColorSpace.release(colorSpace) -- with opaque C values, you have to take care of memory management by yourself
    return gradient  -- returns the gradient, a CGGradientRef value
end

-- Create the gradient and store it in a local variable, used as an upvalue by the drawRect method
local backgroundGradient = createGradientInRGBSpaceWithColorComponents 
                           ({ 251 / 255, 247 / 255, 234 / 255, 1.0,
                              252 / 255, 205 / 255, 063 / 255, 1.0,
                               20 / 255,  33 / 255, 104 / 255, 1.0,
                              181 / 255,  33 / 255, 134 / 255, 1.0 },
                            {0.0, 0.5, 0.5, 1.0})

-- Define a custom drawRect method that draws the gradient in the view
function GradientView:drawRect (rect)
    local ctx = UiGraphics.getCurrentContext() -- ctx contains an opaque CGContextRef value
    local startPoint = { x = 0.0, y = 0.0 }
    local endPoint   = { x = 0.0, y = self.bounds.size.height }
    CgContext.drawLinearGradient (ctx, backgroundGradient, startPoint, endPoint, 0)
end

return GradientView

By setting GradientView as the class of the view in the battery monitor app above, we can see how the gradient background looks like:

The device info ViewController with a GradientView background

Wrapping up

This last part of our Get Started with Lua series is now reaching its end, and I hope that you have a clearer view of what the Bounces native bridge does and how to use it.

If you have read all three articles in the series, you should have everything in hand to start writing a first reasonably complex iOS, tvOS or MacOS application in Lua. And if you feel like you need a quick refresh on any of the topics covered in these articles, you can jump directly to the corresponding section using the tables of contents in the article pages. Actually you can use these Get Started with Lua articles as a quick reference to the Lua language and extensions used in Bounces: they have also been written with this usage in mind.

Where to go next? If you haven't done it yet I strongly recommend watching the Hello World tutorial video or reading the article (available soon) Get started with Bounces. It (will) contains essential information about how to create an application project in Bounces, and how to build this application in live-coding mode, by combining storyboard edits in Xcode and Lua code edits in Bounces, while running the application on your device.

If you need a deeper understanding of the Bounces object framework, the Bounces bridge, or other topics not covered here like application resources updates, or messages and asynchronous apis in Bounces, you can read the Bounces API documentation.

And if you have comments, questions or suggestions about the topics covered in this article, please leave a comment below, or send us an email, or a message on twitter. We will be happy to help.

Post a Comment