Interacting with Objective-C in your Lua program

Celedev Responsive Programming system provides a seamless integration with Objective-C. In this document you'll learn how to use Objective-C classes and objects from Lua, how to take advantage of the bindings libraries for getting all the information you need to benefit from a SDK, as well as how to expose your Lua classes and methods to objective C.

Using Objective-C classes from Lua

Getting a reference to an Objective-C class in Lua is easily done by indexing the global variable objc with the name of the wanted Objective-C class.

Example:

local UIButton = objc.UIButton

This sets the Lua local variable UIButton to contain a reference to the Objective-C class named UIButton.

When used in Lua, an Objective-C class has the same behavior and follow the same conventions as a Lua class defined with the Celedev Object Framework.

Methods and properties

Method names in Lua are derived from Objective-C method names by replacing every : by an underscore _, except the last one.

The table below gives typical examples of method name conversions:

Objective-C method name Corresponding Lua method name
removeFromSuperview removeFromSuperview
alignmentRectForFrame: alignmentRectForFrame
insertSubview:atIndex: insertSubview_atIndex
transitionFromView:toView:duration:options:completion: transitionFromView_toView_duration_options_completion

Methods and properties of Objective-C objects simply use the standard Lua syntax.

aView:insertSubview_atIndex (subview1, 3)
-- myLabel is a UILabel
myLabel.text = "Hello"
local aColor = myLabel.textColor

aView.backgroundColor = aColor
-- this is equivalent to
aView:setBackgroundColor (aColor)

For properties, you have the choice of using the property or method syntax according to your preferences. In the previous example, instead of myLabel.textColor we could have written myLabel:textColor() and this would have been strictly equivalent.

This property notation can also be used for class methods without parameter. For example, for setting the label text color to gray, we can write:

myLabel.textColor = objc.UIColor.grayColor

or the equivalent

myLabel.textColor = objc.UIColor:grayColor()

Instance creation

Objective-C instances can be created in Lua by using new-prefixed constructors. This is exactly the same as with the Celedev Object Framework: when you call anObjcClass:newWithX_y_z(x, y, z), this invokes the classical Objective-C alloc+init sequence in one step like this [[anObjcClass alloc] initWithX:x y:y z:z].

Warning: calling the alloc method of an Objective-C class from Lua is not authorized and can have unpredictable effects. You should always use the new-prefixed syntax instead.

Parameters conversion

In order to provide a good programming experience from Lua, Lua values can often be used as parameters of Objective-C methods. These Lua parameters are converted to the type expected by the method. For example:

local spaceship = objc.SKSpriteNode:spriteNodeWithTexture(self.spaceshipTexture)

spaceship.name = "spaceship"
spaceship.scale = 0.2 * math.random(1,4)
spaceship.position = location
spaceship.zPosition = spaceship.xScale * 100

local radiusX, radiusY = location.x / 2, location.y / 2
local path = CGPath.CreateWithEllipseInRect { x = location.x- 2 * radiusX,  
                                                y = location.y - radiusY, 
                                                width = 2 * radiusX, 
                                                height = 2 * radiusY }
if path then
    local animation = SKAction:followPath_asOffset_orientToPath_duration (path, false, true, 0.5 * math.random(4,10))
    spaceship:runAction(SKAction:repeatActionForever(animation))

    CGPath.Release (path)
end

local waitAction = SKAction:waitForDuration(20)
local shrinkAction = SKAction:scaleTo_duration(0, 1.6)
spaceship:runAction(SKAction:sequence { waitAction, shrinkAction, SKAction:removeFromParent() })

self:addChild(spaceship)

We can see several type conversions in this example:

  • on line 3, the Lua string "spaceship" is converted to a NSString before being assigned to spaceship.name,
  • on line 9, the Lua table { x = … } is converted to a CGRect, the type of the first parameter of CGPathCreateWithEllipseInRect;
  • on line 22, the Lua table { waitAction, shrinkAction, SKAction:removeFromParent() } is converted to a NSArray as it is the expected type for [SKAction sequence:]

The most important type conversions are:

C / ObjC parameter type Accepted Lua Type for conversion
NSArray any Lua table (sequential part)
NSDictionary any Lua table (indexed part)
NSString Lua string
CGPoint Lua table { x = …, y = … }
CGSize Lua table { width = …, height = … }
CGRect Lua table { x = …, y = … , width = …, height = … }
NSRange Lua table { location = …, length = … }
ObjC block Lua function

For performance reasons you may want to convert a Lua table into a NSArray or NSDictionary only once, and use it multiple times later as a parameter. In such case, you can use the convenience conversion methods objc.toArray or objc.toDictionary.

Example:

local metrics = objc.toDictionary { margin = 6, spacing = 10 }

Objective-C blocks are heavily used in recent Objective-C classes. From Lua you simply pass a Lua function to method block-typed parameter.

For example, for calling UICollectionView:

- (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion;

you can use Lua functions like this:

collectionView:performBatchUpdates_completion (function ()
                                                layout.pinchedCellScale = nil
                                                layout.pinchedCellCenter = nil
                                              end,
                                              function (finished)
                                                layout.pinchedCellPath = nil
                                              end)

Return values

Lua functions can have multiple results. This is an elegant replacement for out parameters as used in the C language family.

Celedev system benefits from this feature when calling functions or methods who have out parameters in their C / Objective-C prototype.

For example, NSURL comes with a method - (BOOL)getResourceValue:(out id *)value forKey:(NSString *)key error:(out NSError **)error.

In Lua this is a method with multiple results:

-- Create a NSURL object
local fileURL = objc.NSURL:newFileURLWithPath (path)

local isFound, creationDate, error = fileURL:getResourceValue_forKey_error (iOS.NSURL.CreationDateKey)

if isFound then
    -- use creationDate
    -- …
else
    print (error)
end

Simple and easy.

The same pattern applies for Objective-C blocks with out parameters. Each out parameter of a block is provided as a result of the corresponding Lua function.

To illustrate this, say we want to call from Lua the NSSet method - (NSSet *)objectsPassingTest:(BOOL (^)(id obj, BOOL *stop))predicate; where stop is an out parameter of the block. We can just write the parameter block like this:

local aSet -- a NSSet
local selectedCount = 0
local selectedObjects = aSet:objectsPassingTest (function (object)
                                                     local isSelected = object.isSelected
                                                     if isSelected then 
                                                         selectedCount = selectedCount + 1
                                                     end
                                                     -- stop if 3 selected objects have been found
                                                     return isSelected, selectedCount >= 3
                                                 end)

Playing with collections

Many Objective-C APIs return collections of objects and a common need is to enumerate the returned collection at some point in the program. As enumerations in Lua are naturally done with the for … in … do construct, Celedev supports the enumeration of Objective-C collections with this construct.

General enumeration for all collection classes:

function GameScene:touchesBegan_withEvent(touches, event)
    -- touches is a NSSet
    for touch in touches do
        local sprite = Spearman:createSpearmanAtLocation(touch:locationInNode(self))
        self:addChild(sprite)
    end
end

Indexed collections (like NSArray) can enumerate their indexes an values using the Lua standard function ipairs:

-- allAttributes is a NSArray
for i, attribute in ipairs(allAttributes) do
    self:handleAttribute (i, attribute);
end

Finally keyed collections (like NSDictionary) can enumerate their keys an values using the Lua standard function pairs:

-- options is a NSDictionary here
for key, value in pairs(options) do
    -- handle key and value
end

Extended Lua strings

Lua strings have been extended by Celedev to make them inherit the capabilities of the NSString class. This means that you can both use on a Lua string the methods of Lua strings library and the methods of NSString.

Example:

local monthName = "October"
monthName:lower():drawInRect_withFont(monthRect, monthNameFont)

in which lower() is a function of the Lua strings library and drawInRect_withFont is defined in NSString UIKit additions

Bindings libraries

Bindings libraries have the important role of exposing to your Lua program the public APIs of a C or Objective-C framework or SDK. Exposed entities include constants, enumerations, C functions and C / Objective-C globals variables.

This section explains you how to use a Bindings library in your program so it can have the full access to the corresponding SDK.

A bindings library is organized in Frameworks, each framework containing a number of interface modules, with the same structure as the corresponding C / Objective-C SDK.

You make available a given interface module to your application by calling the Lua function require with the interface module path (separated by .) as its parameter:

local uiview = require "UIKit.UIView"

This call to require returns a table containing all references to constants, enumerations, C functions and C / Objective-C globals variables declared in the UIView module. We store this table in a local variable for using later in our requirer module.

Generally (but this is the choice of the designer of the bindings library) the module table, i.e; the result of the require function is also stored in a global variable to make it easier to retrieve later. In the case of the iOS SDK bindings, this global variable is called iOS, so the module table can be also retrieved as iOS.UIKit.UIView.

Then any value or reference defined by the loaded bindings module can easily be found in the module table.

As an exemple, here is an extract of the module table for "UIKit.UIView", corresponding to the file UIView.h:

local UIView = {
    AnimationCurve --[[@def enum(UIViewAnimationCurve)]] = { EaseInOut = 0,
                                                             EaseIn = 1,
                                                             EaseOut = 2,
                                                             Linear = 3,
                                                           },
    Autoresizing --[[@def enum(UIViewAutoresizing)]] = { None = 0,
                                                         FlexibleLeftMargin = 1,
                                                         FlexibleWidth = 2,
                                                         FlexibleRightMargin = 4,
                                                         FlexibleTopMargin = 8,
                                                         FlexibleHeight = 16,
                                                         FlexibleBottomMargin = 32,
                                                       },
    -- ...
    UILayoutConstraintAxis --[[@def enum(UILayoutConstraintAxis)]] = { Horizontal = 0,
                                                                       Vertical = 1,
                                                                     },
    NoIntrinsicMetric --[[@type number]] = 0.0 --[[ C extern const variable]],
    UILayoutFittingCompressedSize --[[@type struct(CGSize)]] = nil --[[ C extern const variable]],
    UILayoutFittingExpandedSize --[[@type struct(CGSize)]] = nil --[[ C extern const variable]],
}

return UIView;

If we need to use the UIView resizing mask constants in our program, we just have to get them from the table:

myView:setAutoresizingMask(uiview.Autoresizing.FlexibleLeftMargin + uiview.Autoresizing.FlexibleRightMargin)

Here is another example with C functions defined in bindings, using Core Graphics:

local CGPath = require "CoreGraphics.CGPath"

The CGPath module table contains the function CreateWithEllipseInRect:

local CGPath = {
    CGLineJoin --[[@def enum(CGLineJoin)]] = { Miter = 0,
                                               Round = 1,
                                               Bevel = 2,
                                             },
    CGLineCap --[[@def enum(CGLineCap)]] = { Butt = 0,
                                             Round = 1,
                                             Square = 2,
                                           },
    --...
    CreateWithRect = function (rect --[[@type struct(CGRect)]], transform --[[@type external(const CGAffineTransform *)]]) --[[C function]] --[[@return external(CGPathRef)]] end,
    CreateWithEllipseInRect = function (rect --[[@type struct(CGRect)]], transform --[[@type external(const CGAffineTransform *)]]) --[[C function]] --[[@return external(CGPathRef)]] end,
    CreateWithRoundedRect = function (rect --[[@type struct(CGRect)]], cornerWidth --[[@type number]], cornerHeight --[[@type number]], transform --[[@type external(const CGAffineTransform *)]]) --[[C function]] --[[@return external(CGPathRef)]] end,
    --...
}
return CGPath   

So we can create a CGPath in Lua by writing:

local path = CGPath.CreateWithEllipseInRect { x = location.x- 2 * radiusX,  
                                             y = location.y - radiusY, 
                                             width = 2 * radiusX, 
                                             height = 2 * radiusY }

Note: Bindings libraries are also needed when your Lua program calls an Objective-C method, but this is handled transparently by Celedev system and all you have to do for it to work is to link the corresponding Bindings library with your application.

Post a Comment