Using Lua Modules in your program

Lua modules, and the require function, are fundamental elements of Celedev Responsive Programming System. Associated with the Celedev Object Framework and the Dynamic Code Update feature, Lua modules bring flexibility and power to your program.

The require function

Each independent piece of code that Lua compiles and executes is called a chunk. A chunk can be read from a file or can simply be a string in memory. Lua handles a chunk as the body of an anonymous function with a variable number of arguments; as such chunks can define local variables, receive arguments, and return values.

Lua modules and the require function are the privileged way of loading Lua chunks from other Lua chunks, by identifying a chunk to be loaded by a logical name. The Lua modules mechanism extends to native modules written in C or any similar language that provide a way to add libraries to the Lua language.

Note the require function in the Celedev Responsive Programming system has the same interface as the standard Lua require, but implements a slightly different behavior, better adapted to the dynamic apps development. Therefore most of the information found about Lua modules on the web is irrelevant to Celedev implementation.

The require function loads, compiles, and executes a Lua module with the given name and returns the module's results to the caller.

require (moduleName)

Parameters:

  • moduleName: the name of the required module (a string). The module name can be a dot-separated path like iOS.UIKit.UIView. The rules for searching a module by name or by path are explained below.

Returns:

  • one or more module results, which are simply the results returned by the module chunk. Module results can be of any type, but are supposed to be singleton entities, such as a Lua table defining a library, or class defined with Celedev Object Framework.

require performs the loading and execution of a module only once in a given Lua Context: the results returned by this module execution are stored internally by the Lua Context, and when an other require for the same module is made later, the module's results are directly returned to the caller, without reloading the module.

This is illustrated by the following example:

-- first call to require for module named "CheckerViewController": 
-- compiles and execute the corresponding Lua chunk
local CheckerViewController = require "CheckerViewController"

--...

-- a second call to require for the same Lua module just return the module's results
local anotherCheckerViewController = require "CheckerViewController"

-- This assert will succeed
assert(anotherCheckerViewController == CheckerViewController)

The recommended way of using require is to store its result in a local variable and then to use this variable in your program. Although most Lua modules also store their result in a global variable, using local variables for modules is good for performances and increases the program readability.

Example of use:

-- require the module defining the CheckerViewController class  
local CheckerViewController = require "CheckerViewController"

-- use the CheckerViewController class stored in the local variable
local checkerController = CheckerViewController:new()
navController:pushViewController_animated(checkerController, true)

Note that require does not pass any argument to a module chunk. Therefore it it useless to read the chunk's arguments in a module using the ... syntax, as this will always result in a nil value.

Module search algorithm

When require is called, the Celedev module loader searches for a Lua code file matching the provided module name in an ordered list of possible locations. These module locations are, from highest to lowest priority:

  1. the caller's module location if any (see below);
  2. the main source package associated to the current Lua Context (defined by the mainSourcePackageId parameter when the Lua Context was created (see Celedev CIMLua API);
  3. other source packages associated to the current Lua Context (defined in Objective-C with the method -[CIMLuaContext addAccessToSourcePackagesWithIds:]);
  4. the application's main bundle: .lua files in the application bundle can be loaded with require;
  5. the Lua bindings libraries linked with the application.

The caller's module location rule deserve some explanation: it simply means that if the caller of the require function is a Lua module, the search for the required module will first be done in the same source package as the caller module. The reason for this is locality and encapsulation: if a module with the given name exists in the same source package as the caller, we suppose that this is the one that is required.

A module name in the require function is dot-separated path that defines the name and directory of a module inside its location. Path elements in a module name are case-sensitive and shall not have a file extension.

To illustrate this, let's suppose that the main source package of the current Lua Context has the following structure:

|- CheckerViewController.lua
|- CheckerView.lua
|- Layouts  |
            |- Circle |
            |         |- Small.lua
            |         |- Large.lua
            |- SquareLayout.lua

then the modules in this package will be required with the paths:

local CheckerViewController = require "CheckerViewController"
local CheckerView = require "CheckerView"

local SmallCircleLayout = require "Layouts.Circle.Small"
local LargeCircleLayout = require "Layouts.Circle.Large"

local SquareLayout = require "Layouts.SquareLayout"

Modules and dynamic code update

Celedev Dynamic Code Update works at the module level.

This means that when the code of a module is changed in the IDE with the automatic update mode activated, this module's chunk is reloaded by the Lua Context and the new version of the module's results replace the old ones.

This also means that Lua code loaded without using the require function (or the equivalent -[CIMLuaContext loadLuaModuleNamed:withCompletionBlock:]) can not benefit from the advantages of dynamic code update!

Note that if some portion of your Lua code keeps a direct reference to a function or data created by a module, such direct reference won't be updated automatically when the module is reloaded, and may thus refer to an outdated version of the module. This won't cause your application to crash since these references are still valid, but this is probably not what you expect.

To avoid such outdated references issues, the best strategy is to use the Celedev Object Framework in your program: the Celedev Object Framework is designed to be fully compliant with dynamic code updates and will keep your references always up-to-date with the latest versions of the loaded modules.

Class definition modules

A good practice when you define classes with the Celedev Object Framework is to create one module per class. Actually keeping your modules at the class level is really appropriate for dynamic update, both in terms of logical program architecture and for keeping the granularity of your modules small and the speed of the updates fast.

In terms of structure, a class definition module usually follows a simple pattern:

  1. it first does every needed require for other modules
  2. then it creates (or recreates) the class by calling class.createClass
  3. then it implements the class methods, getters, setters, properties …
  4. finally it returns the class as the module result

Here is a comple example of a class definition module illustrating the pattern above:


local UITableViewCell = require 'UIKit.UITableViewCell' local NSLayoutConstraint = require 'UIKit.NSLayoutConstraint' local CGGeometry = require 'CoreGraphics.CGGeometry' local NSLayoutAttribute = NSLayoutConstraint.NSLayoutAttribute local NSLayoutRelation = NSLayoutConstraint.NSLayoutRelation local UITableViewCellClass = objc.UITableViewCell local CheckerTableCell = class.createClass ('CheckerTableCell', UITableViewCellClass) function CheckerTableCell:initWithStyle_reuseIdentifier(style , reuseIdentifier) self = self:doAsClass ('initWithStyle_reuseIdentifier', UITableViewCellClass, style , reuseIdentifier) if self ~= nil then self.accessoryType = UITableViewCell.AccessoryType.DetailDisclosureButton local cellBackgroundColor = self:backgroundColor() -- Cell's label self.textLabel:setTranslatesAutoresizingMaskIntoConstraints(false) self.textLabel.backgroundColor = cellBackgroundColor self.textLabel.opaque = false self.textLabel.textColor = objc.UIColor:blackColor() self.textLabel.highlightedTextColor = objc.UIColor:whiteColor() end return self end local fontName = "AvenirNext-Italic" local checkedImage local uncheckedImage local metrics = objc.toDictionary { margin = 8, spacing = 15 } function CheckerTableCell:layoutSubviews () self:doAsClass ('layoutSubviews', UITableViewCellClass) local contentView = self.contentView contentView:removeConstraints(contentView.constraints) local views = objc.toDictionary { button = self.checkButton, text = self.textLabel } local horizConstraint = objc.NSLayoutConstraint:constraintsWithVisualFormat_options_metrics_views ("|-margin-[button]-spacing-[text]-(>=spacing)-|", 0, metrics, views) local vertButtonConstraint = objc.NSLayoutConstraint:constraintsWithVisualFormat_options_metrics_views ("V:|-margin-[button]-margin-|", 0, metrics, views) contentView:addConstraints (horizConstraint) contentView:addConstraints (vertButtonConstraint) local textMargin = metrics.margin * 1.6 local fontSize = contentView.bounds.size.height - 2 * textMargin self.textLabel.font = objc.UIFont:fontWithName_size(fontName, fontSize) end function CheckerTableCell:toggleCheck (sender) self.checked = not self.checked self._itemInfo.checked = self.checked end CheckerTableCell:publishActionMethod ("toggleCheck") CheckerTableCell:declareSetters { itemInfo = function (self, itemInfo) self._itemInfo = itemInfo self.textLabel.text = itemInfo.text self.checked = itemInfo.checked end, checked = function (self, isChecked) self._checked = isChecked self.checkButton:setBackgroundImage_forState (isChecked and checkedImage or uncheckedImage, UIControl.State.Normal) end } CheckerTableCell:declareGetters { itemInfo = function (self) return self._itemInfo end, checked = function (self) return self._checked end } getResource ('data.checked', 'png', function (resourceImage) checkedImage = resourceImage end) getResource ('data.unchecked', 'png', function (resourceImage) uncheckedImage = resourceImage end) return CheckerTableCell

Post a Comment