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.
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:
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:
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