Lua for App Development

Lua modules in Bounces

Lua modules are key components for Lua development in Bounces. In this document, you will learn how to use the require function to load Lua modules in a Bounces project, how require in Bounces differs from standard Lua, and how to make your Lua modules nicely play with dynamic code update.

Lua modules in Bounces

In Bounces, you load Lua code modules by calling the require function, as illustrated by the various code samples in this document or in that one.

For various reasons, and in particular to enable dynamic code update, Bounces provides a custom implementation of the require function. This custom implementation has some differences compared to the standard Lua version, and these are detailed in this section.

All Lua files in a project are modules

In Bounces, you organize the Lua code in a project as a tree of Lua source files. Each of these Lua source files is treated as a Lua module, so you use Lua global function require to load a module in any other Lua source file that depends on this module.

Typically, every class or class-extension defined in a project is stored in an individual module. In addition, a Bounces project generally includes a Main or Start module, executed by the target app's native code and in charge of loading (by calling require) and initializing other project modules.

As we have seen in the Lua language overview, a Lua module's code is executed only the first time require is called and subsequent calls to require for the same module return the memorized module's result without executing the module's code again. This means that a module shall only return reusable entities expected to remain constant across multiple require calls, like a class, a shared Lua table or a function.

For example, a typical class extension module could look like this:

-- Module MyViewControllerAnimations.lua

-- get a reference to native class 'MyViewController' and declare a class extension on it
local MyViewController = objc. MyViewController:addClassExtension "CustomAnimations"

-- require project modules needed for the class extension implementation
local CollectionView = require 'UICollectionView'
local LargeCircleLayout = require 'Layouts.Circle.LargeCircle'

-- define methods and properties for this extension

function MyViewController:setCircleLayout()
    self.layout = LargeCircleLayout:new()

-- define other methods...

-- return the extended class
return MyViewController

This module returns the extended class MyViewController, which is uniquely defined in the target application. Therefore requiring this module multiple times as in local MyViewController = require 'MyViewControllerAnimations' will always return the same class reference, which is precisely what is expected in this case.

Modules can return multiple values

A Lua function can return multiple results, and and sometimes a Lua module can benefit from multiple return values as well.

Therefore, in Bounces, a Lua module chunk can return multiple values, like any Lua other function. require returns transparently all results returned by the module's chunk, and returns nil if the module's chunk does not return anything.

For example, this Controller module returns a function and a string:

-- A module named 'Controller' in your project
local function createController (controllerName) 
    return { name = controllerName } 

local createdMessage = "Controller has been created"

return createController, createdMessage

These results are returned by require when loading the module:

-- The caller module gets all results via the require function
local createFunction, controllerMessage = require "Controller"

local firstController = createFunction ("controller-1") -- firstController --> { name = "controller-1" }
local secondController = createFunction ("controller-2") -- secondController --> { name = "controller-2" }

print(controllerMessage) --> Controller has been created

-- If we require the Controller module again, memorized module results are returned
-- (the module chunk function is NOT executed a second time)
local createFunction2, controllerMessage2 = require "Controller"

print (createFunction2 == createFunction) --> true

Modules are searched in the current project

In Bounces, the module-name parameter in a call to require is considerered as a path in the current project source tree.

A module path is a dot-separated string. For example, 'Layouts.SquareGrid' is a valid module path specifying a Lua module named 'SquareGrid' in a group name 'Layouts'.

The module path can be absolute or relative to the caller module's group in the project source file tree. Using relative modules paths provide better locality and are more robust in case of parent group renaming.

Consider a Bounces project with the following source tree:

|- StartModule.lua
|- MyViewController.lua
|- MyViewControllerAnimations.lua
|- UICollectionView.lua
|- Layouts  |
            |- Grid.lua
            |- SquareGrid.lua
            |- Circle |
            |         |- SmallCircle.lua
            |         |- LargeCircle.lua

|- OS       |- ...
|           |- UIKit    |- ...
|                       |- UICollectionViewFlowLayout.lua
|                       |- UICollectionView.lua
|                       |- ...
|- MyApp    |
            |- MyApp_Swift.lua

And here are some calls to require in this project and the resolved modules for the corresponding path:

-- Module StartModule
require 'MyViewController' --> PROGRAM FILES > MyViewController.lua
require 'MyViewControllerAnimations' --> PROGRAM FILES > MyViewControllerAnimations.lua
-- Module MyViewController
local CollectionView = require 'UICollectionView' --> PROGRAM FILES > UICollectionView.lua
local LargeCircleLayout = require 'Layouts.Circle.LargeCircle' --> PROGRAM FILES > Layouts > Circle > LargeCircle.lua
require ' MyApp_Swift' --> BINDINGS LIBRARIES > MyApp > MyApp_Swift.lua
-- Module  UICollectionView
Local UICollectionView = require 'OS.UIKit.UICollectionView' --> BINDINGS LIBRARIES > OS > UIKit > UICollectionView.lua
-- Module  SquareGrid
local Grid = require 'Grid' --> PROGRAM FILES > Layouts > Grid.lua
Local UICollectionViewFlowLayout = require 'UIKit.UICollectionViewFlowLayout' --> BINDINGS LIBRARIES > OS > UIKit > UICollectionViewFlowLayout.lua

You can notice that:

  • The search is first done in the current module's group, then in the current top-level group (typically PROGRAM FILES), and finally in other top-level groups, like BINDINGS LIBRARIES entries.
  • Bindings library names can be omitted in the module path, when no ambiguity exist: for example require 'UIKit. UICollectionViewFlowLayout' is resolved as BINDINGS LIBRARIES > OS > UIKit > UICollectionViewFlowLayout.lua.

Actually, Bounces's module search policy is optimized for apps development and remote modules loading from the IDE, making it quite different from the search policy implemented in standard Lua. As a consequence, Lua's standard package library is not relevant, and therefore not available, when developing with Bounces.

Modules are dynamically updated

Dynamic code update is a core feature of Bounces. When a module's code is changed, this module can be manually or automatically reloaded to update the corresponding code in the target application.

Reloading a module is not different than loading it for the first time. The updated module code chunk gets compiled and executed, and results of the updated module are stored in the modules-results data storage, replacing those of the modules previous version. If require is called later on this module, these updated results will be returned.

You have to be aware that module update is intentionally kept local to the updated module, for the sake of robustness and simplicity, so requiring modules are not re-executed when one of their required modules is updated.

Therefore the simplest way to gracefully handle Lua modules updates in your program is to write modules that return the same Lua value(s) as the previous version when updated. Typically by having the module return a shared or global Lua value and making the updated module code only changes the internals of this shared Lua value, not the value itself.

This is precisely what the Bounces object framework does, so building your modules around the object framework and returning a class object is the best guaranty that dynamic modules updates will just work, without any unwanted side effect.

In the rare cases where returning a shared Lua value in a module is not an option —for example if the module returns a Lua function— and you need to support dynamic code update for such a module, you can register for module update notification messages in the requiring module(s) and, when notified, call require again to get the updated module's new return value.

Related documentation

Post a Comment