Using resources in your program

Celedev Responsive Programming system includes an extensive support for application Resources of all kinds. Resources can be loaded in the program with the getResource function and are dynamically updated in the application when they are modified, just as Lua modules.

Resources in the Celedev system

It takes more than just code to build a great application. Resources are the additional files and static content that your code uses, such as images, layout definitions, user interface strings, and more.

That's why Celedev Responsive Programming system includes an extensive support for Resources of all kinds.

The getResource function

The getResource function is the equivalent of the require function, applied to resources. getResource loads a resource, known by its resource name, into a program and handles the caching and dynamic update of this resources in a Lua Context.

getResource can be used in several flavors:

getResource (resourceName, [resourceType])
getResource (resourceName, [resourceType], handlerFunction)
getResource (resourceName, [resourceType], handlerObject, objectFieldName)

Parameters:

  • ResourceName: the name of the required resource (a string). The resource name can be a dot-separated path like Images.Button1. The rules for searching a resource by name or by path are explained below.
  • resourceType: the type of the required resource, either as a Uniform Type Identifier (UTI) or as a file extension (optional, a string). Use a UTI if you want more flexibility in the type specification, for example "public.image".
  • handlerFunction: a Lua function that will be called when the resource is loaded, and then on every dynamic update of this resource. It takes the resource object as its sole parameter: handlerFunction(resourceObject).
  • handlerObject (an instance of a Celedev Framework class, an instance of an Objective-C class, or a Lua table), objectFieldName (a string): the reference of an object property that will be set when the resource is loaded, and then on every dynamic update of this resource. Setting the object property is equivalent to executing the statement: handlerObject.objectFieldName = resourceObject. Usually the object property has an associated setter that is called when the resource is loaded or updated, but it can be a plain simple object field as well.

Returns:

  • the resource object, or nil if the resource is not found.

getResource handles a cache for storing resource objects, so that calling it again later on the same resource does not involve the performance penalty of reloading and recreating the resource.

Examples of use:

-- getting resource without handler
local bananaImage = getResource ("banana")
local orangeImage = getResource ("orange", "png")
local appleImage  = getResource ("apple", "public.image")

-- getting resource with an handler function
-- say we want the upvalue bananaImage to always be up to date when FruitHandler:showFruit is called

local bananaImage
getResource ("banana", "public.image", function (resourceImage) bananaImage = resourceImage end)

function FruitHandler:showFruit()
    self.imageView.image = bananaImage
end

-- getting resource with an handler object field
-- by associating the field with a setter, we can refresh self.imageView (a UIImageView) whenever the orange image is updated

function FruitHandler:viewDidLoad()
    getResource ("orange", "png", self, "_imageView")
end

FruitHandler:defineSetters { _imageView = function (self, image) self.imageView.image = image end }

-- we can replace the above example by this simpler code directly setting the "image" Obj-C property 
-- of the UIImageView self.imageView in the getResource call (this will directly update 
-- self.imageView.image whenever the orange image changes, without needing to declare a setter)

function FruitHandler:viewDidLoad()
    getResource ("orange", "png", self.imageView, "image")
end

When called with a handler object or function parameter, getResource creates a new association with the provided handler. A given resource can be associated with any number of handler objects or functions, but a resource can have at most one association with each handler object or function. This means that an object can only associate a single field with a given resource, as shown in this example:

-- object1 is an object

getResource ("anImage", "jpg", object1, "field1") 
-- resource "anImage" is now associated with object1.field1

getResource ("anImage", "jpg", object1, "field2") 
-- this replaces the existing association between resource "anImage" and object1
-- resource "anImage" is now associated with object1.field2 (and not with object1.field1 anymore)

The lifetime of the association between a resource and a handler depends on the type of the handler:

  • for a handler function, the association lasts until the code module where getResource was called is updated; this behavior avoids keeping lots of zombie handler functions associated with a resource when a code module is frequently updated;
  • for a handler object, the duration of the association follows the object lifecycle: the association is removed when the handler object is deallocated (or garbage collected if the object is a Lua table); note that associating a field of an object with a resource has no impact on object lifecycle (in other words, the association creates a weak reference to the object).

Resource search algorithm

When getResource is called, the Celedev resource loader searches for a resource file in the program matching the provided resource name and type in an ordered list of possible locations.
Note: this search process is identical to the search process for Lua modules with the require function, with the difference that the type of search resource is not a Lua source file (see Using Lua Modules).

The searched 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: resource files in the application bundle can be loaded with getResource;

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

A resource name in the getResource function is dot-separated path that defines the name and directory of a resource inside its location. Path elements in a resource 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
|- data.plist
|- Images   |
            |- Fruits |
            |         |- banana.jpeg
            |         |- prange.png
            |- button.jpg

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

local data = getResource ("data")
local button = getResource ("Images.button")
local banana = getResource ("Images.Fruits.banana")

Resource generators

When a Resource is loaded, it would not be convenient for the program to receive this resource as a bunch of raw data. So, to provide the resource in a natural format to the program, Celedev relies on Resources Generators that converts resource data of a given type into an appropriate resource object.

A small set of commonly-used Resource Generators is provided with the system. And you can replace these built-in Resource Generators or add your own if you wish.

If no Resource Generator is defined for the type of the received resource, the provided resource object is of class NSData.

Built-in resource generators for iOS are:

Resource Type Resource Object type or class
public.plain-text Lua string
public.image UIImage
plist file NSDictionary
nib file UINib
undefined NSData

Defining your own Resource Generators is done in the Objective-C part of your application by using the method +[CIMLuaContext registerResourceConstructors:]. It takes a dictionary parameter that associate resource types (UTI or extension) with CIMLuaResourceConstructor blocks.

A CIMLuaResourceConstructor is declared as

typedef id (^CIMLuaResourceConstructor) (NSURL* resourceURL, NSData* resourceData);

Parameters:

  • resourceURL: a file URL where the resource can be read
  • resourceData: a possibly-nil NSData containing the resource raw data; if resourceData is nil, you have to read the data from resourceURL; else, if appropriate, creating the resource from resourceData will be much faster since you don't need to access the file system.

Result:

  • the resource object

Examples of Resource Generators declarations:

CIMLuaResourceConstructor plainTextResourceConstructor = ^id(NSURL* resourceURL, NSData* resourceData) {
    NSString* resourceObject;
    if (resourceData != nil) {
            resourceObject = [[NSString alloc] initWithData:resourceData encoding:NSUTF8StringEncoding]; // Suppose UTF8 encoding
    }
    else {
            resourceObject = [NSString stringWithContentsOfURL:resourceURL encoding:NSUTF8StringEncoding error:NULL];
    }
    return resourceObject;
};

CIMLuaResourceConstructor imageResourceConstructor = ^id(NSURL* resourceURL, NSData* resourceData) {
    UIImage* resourceObject;
    if (resourceData != nil) {
            resourceObject = [UIImage imageWithData:resourceData];
    }
    else {
            resourceObject = [UIImage imageWithContentsOfFile:resourceURL.path];
    }
    return resourceObject;
};

CIMLuaResourceConstructor nibResourceConstructor = ^id(NSURL* resourceURL, NSData* resourceData) {

    NSBundle* resourceBundle = [NSBundle bundleWithURL:[resourceURL URLByDeletingLastPathComponent]];
    NSString* nibName = [resourceURL.lastPathComponent stringByDeletingPathExtension];

    return [UINib nibWithNibName:nibName bundle:resourceBundle];
};

[self registerResourceConstructors:@{ (NSString*)kUTTypePlainText: [plainTextResourceConstructor copy],
                                      (NSString*)kUTTypeImage:     [imageResourceConstructor copy],
                                      @"nib":                      [nibResourceConstructor copy],
                                      }];

Post a Comment