Mastering live-coding with CodeFlow (2)

To use the live-coding feature of CodeFlow to its full potential, it is useful to understand the process that transforms a change in the source code into a live update of your application, and how you can control this process. Such is precisely the goal of this series of two articles: to give you the keys to become a live-coding master.

In the first part of this article, we have seen what happens when a Lua module / resource is loaded or reloaded, and we know how to be notified when such (re)loads occur.

In this second part we will review various strategies to to refresh your application when receiving such a reload event. But first, it can be interesting to look more in detail at the state of the application after a reload.

Application state after a Lua module reload

As an example, we will do a code update in a small UICollectionView-based application running on an iPhone. This application defines a CollectionController class whose methods are implemented in two different Lua modules:

  • a module named CollectionController where the class is created and most of its methods are defined,
  • a module name CollectionController+Gesture that declares a class extension and contains a handful of gesture-related methods.

Here is how the CollectionController class is displayed in CodeFlow variable inspector:

Class methods before module reload

You can see in the left column the names of the defined methods for this class and in the right column the current values of these methods, which are actually the Lua functions defined in the two Lua modules implementing the CollectionController class.

Now let's change the code of the handlePinchGesture method defined in the CollectionController+Gesture Lua module. This module gets reloaded into the running target application and we can see the updated content of the CollectionController class in the variable inspector:

Class methods after module reload

The variable inspector highlights the changed values and shows that the three methods defined in the CollectionController+Gesture module have been updated while other methods of this class have not changed. This is actually what we could expect: although we changed only the handlePinchGesture method, the entire CollectionController+Gesture module has been reloaded and executed, and the execution has re-created the 3 method functions that are now at different addresses.

So after the module is reloaded, any invocation of a gesture-related method on a CollectionController instance will call the updated method functions instead on the old ones, and specifically, any invocation of the handlePinchGesture method will execute the changed code and multiply the scale by 2!

However, if one of the replaced methods was being executed when the module was reloaded, the execution of the corresponding method function is unaffected by the module reload and it will continue normally (i.e. with the old code) until it returns.

To visualize this, we can set a breakpoint in the handlePinchGesture method, and pinch in the application to make it stop on this breakpoint.

Stopped on breakpoint before module reload

Then we change the code and replace gestureRecognizer.scale * 2 by gestureRecognizer.scale / 3. The module gets reloaded, but if we click on the callstack, we can see that we are still stopped at the same place in the old code:

Stopped on breakpoint after module reload

In addition, you can see that the display have changed: the source code background color is different and the selected item in the side bar is not a Source File anymore, but is a Loaded Item and more precisely the loaded version of the CollectionController+Gesture module defining the currently executed function. This is how CodeFlow shows you that the application is not currently executing the latest version of a Lua module. (Note: to make this clearly visible, I have set a custom read-only background color in CodeFlow preferences; you can do this too if you want.)

The important point in this case is that, until we return from this method, we are still executing the old code. So we better click on Continue. And then, the next time the handlePinchGesture method is invoked, we can see, when the execution stops, that it is now running the new version of the module.

Stopped on next breakpoint after module reload

Application refresh strategies

Tu sum up what we have seen previously, when we change the code in a Lua module, this module gets reloaded in the target application, and the result of this reload is to update , in their target class, the methods defined in the loaded module. Then when one of these methods is called, the updated method functions are executed and everything behaves well.

Is this sufficient to claim that we achieve a true live-coding experience? Almost but not entirely. Actually, what we generally expect when live-coding an application is to have the application display, react and behave as if the current code and resources has been there since the application launch.

Therefore what we need to do is to refresh the application state and display, to make it reflect the current code and resources. What refreshing the application truly means heavily depends on the application. Therefore, the right application refresh strategy will vary from application to application, and from screen to screen or even from class to class inside a given application.

The good news is that, usually, refreshing the application is very simple and straightforward, and there are good chances that the necessary code for it is already in your application!

So let's see how to do it.

Setup a reload handler

Basically the things we need to add to a class so that it supports live coding are:

  1. make every instance of the class subscribe to the appropriate reload event; this subscription to the reload event shall be placed in a method preferably called once for every instance object, typically an initializer, a viewDidLoad method for a view controller…
  2. define at least one method to handle the reload event (i.e. a reload handler).

For example, in a view controller class, a reload handler could be setup like this:

local superclass = ViewController.superclass

function ViewController:viewDidLoad ()
    self[superclass]:viewDidLoad()

    -- put you specific code here
    -- ...

    -- subscribe to the general load-module message for this instance
    self:addMessageHandler ("system.did_load_module", "refresh")
end

-- ...

function ViewController:refresh()
    -- Your reload handler code here
    -- ...
end

return ViewController

It is interesting to note that the reload handler (the refresh method in the above example) is a regular method of the class. As such you can live-code the reload handler and apply different refresh patterns depending on the changes you've done in other class methods. Since the reload notification message is received after the module update is done, the invoked reload handler will always be the most up-to-date one!

Common refresh patterns

This section presents the most common application refresh patterns. Although they are presented separately for clarity, you are of course free to mix them according to your needs.

Refresh the display or layout of views or other objects

This refresh pattern is simple and may be enough in many cases.

For example, in a view or view controller class, you will call setNeedsDisplay() or setNeedsLayout() or setNeedsUpdateConstraints() depending on what you need to update.

function ViewController:refresh()
    self.view:setNeedsDisplay()
end

Or when live-coding a UICollectionViewLayout subclass, you will set this class reload handler as:

function MyCollectionLayout:refresh()
    self.invalidateLayout()
end

Reload a collection view

In a TableViewController or CollectionViewController, it is really easy to handle a reload of code or data by reloading the entire collection.

This may not be the most optimal solution in terms of performances, but usually this doesn't really matter as the live-coding doesn't cause very frequent reload events.

Here is an exemple in the context of a UITableViewController:

function MyTableViewController:refresh()
    self.tableView:reloadData ()
end

Create a dedicated configure method for the class

Often in your Lua class, you'll want to live-code parts of the code that are normally just called once, for example if you want to attach new object fields to the instances of a class (like subviews, behaviors, gesture recognizers…).

In such case the recommended way of proceeding is to create a dedicated configure method that will fully configure an instance of the current class. This configure method is invoked from the appropriate location in the code, i.e. when the object is created or loaded, and is also called after each code reload of the current class. So that, if you change the code of the configure method, the new version vill be called on every instance of the class.

In a configure method, you should avoid to create multiple times the instance's object fields. The easiest way to do this is to use the create-if-not-exists pattern: it consists in storing the objects used by an instance in a field of this instance, and by creating them in the configure method only if this field is non-nil.

An example should make this clear: once again, let's use a UIViewController in this example. The initial code looks like this

local superclass = ViewController.superclass

function ViewController:viewDidLoad ()
    self[superclass]:viewDidLoad()

    -- call the configure method
    self:configureController()

    -- subscribe to a custom load-module message for this instance
    self:addMessageHandler ("my controller module was loaded", "refresh")
end

function ViewController:configureController ()
end

-- ...

function ViewController:refresh()
    -- Update the configuration for this instance
    self:configureController()
    -- and redisplay the view
    self.view:setNeedsDisplay()
end

-- post the custom load-module message
message.post ("my controller module was loaded")

return ViewController

In this example, the configureController method is initially empty. Let's write it in live-coding mode, while the application is running.

First we create an image view and add it to the controller view. We can do this by updating the configureController method, like this:

function ViewController:configureController ()
    -- create the image view if it does not exists
    if self.imageView == nil then
            local imageView = objc.UIImageView:newWithFrame(imageViewRect)

            -- Store the view in self and add it to the superview
            self.imageView = imageView
            self.view:addSubview(imageView)
    end

    -- configure the image view
    local imageView = self.imageView
    getResource(imageName, 'public.image', imageView, "image")
end

Here the image view is created only once, when self.imageView == nil. But it is re-configured with an image resource after each code update. When the ViewController module is reloaded, the new version of configureController is called for every ViewController instance and the image view is added to all of them.

Let's go further and add a gesture controller to the image view;

function ViewController:configureController ()
    -- create the image view if it does not exists
    if self.imageView == nil then
            local imageView = objc.UIImageView:newWithFrame(imageViewRect)

            -- Store the view in self and add it to the superview
            self.imageView = imageView
            self.view:addSubview(imageView)
    end

    -- configure the image view
    local imageView = self.imageView
    getResource(imageName, 'public.image', imageView, "image")
    imageView.userInteractionEnabled = true

    -- Create the image pan recognizer if it doesn't exist yet
    if self.imageViewPanRecognizer == nil then
        self.imageViewPanRecognizer = objc.UIPanGestureRecognizer:newWithTarget_action(self, "panImageView");
        imageView:addGestureRecognizer(self.imageViewPanRecognizer)
    end
end

function ViewController:panImageView(gestureRecognizer)
    -- ...
end
ViewController:publishActionMethod ('panImageView')

When the ViewController module is reloaded, the pan gesture recognizer is added to the image view and the panImageView method is added to the ViewController class and published as action method). Note that we have added imageView.userInteractionEnabled = true to the image view configuration, so that the gesture recognizer receives touch events.

At this point, if we relaunch the application, the current version of the configureController method will be called from viewDidLoad and will create directly the image view and its pan gesture recognizer.

Dispatch the update event to other objects

Dispatching the update event is helpful for resource updates, when we can not use the getResource function for updating directly the objects using a resource. The principle here is to have a controller object subscribing to the resource, and when the resource is updated, the controller object enumerates the resource user objects and applies the new version of the resource to each of them.

For example, suppose we want to apply a live image texture to spaceship nodes in a SpriteKit game. We could write the code below:

function GameScene:configureScene ()
    -- ...
    getResource ('Spaceship', 'png', self, "spaceshipImage")
end

function GameScene:addSpaceshipAtLocation (location)

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

    -- Configure the spaceship node
    -- ...

    self:addChild(spaceship)
end

function GameScene:setSpaceShipImage (resourceImage)
    self.spaceshipTexture = SKTexture:textureWithImage(resourceImage) 

    self:enumerateChildNodesWithName_usingBlock("spaceship",
                                                function (node) 
                                                    node.texture = self.spaceshipTexture 
                                                end)
end

GameScene:declareSetters { spaceshipImage = "setSpaceShipImage" }

A few comments about this example:

  • Line 3: The GameScene object subscribes to the PNG resource named SpaceShip in the CodeFlow project and associates it with its spaceshipImage property.
  • Line 26: the spaceshipImage property is actually a setter that calls the setSpaceShipImage method declared on line 17.
  • Line 18: when the SpaceShip resource is set or updated, the GameScene object create a SKTexture with the image and stores this texture in a field self.spaceshipTexture. This spaceshipTexture field is used when a spaceship is created (on line 8). It is also used in the setSpaceShipImage method to update every existing spaceship nodes with the new texture (on lines 20 to 23).

Wait to be called

The wait-to-be-called application refresh strategy (also called the lazy-live-coder refresh strategy) is perfectly valid when updating a method that is called periodically, or is at least certainly called under some known conditions. As its name suggests, this easy-to-use application refresh method does not require any specific refresh code, because the system will call the updated method anyway.

A good example of periodically called method is SceneKit's SCNSceneRendererDelegate:renderer_didSimulatePhysicsAtTime(). This method is called for every frame rendered by SceneKit, so we can live-code it directly in one of our classes, without any additional work.

An other type of wait-to-be-called-compliant methods are action methods: sent by a user interface object, it generally wouldn't make sense to call them for the sake of refreshing the application. And on the other hand, testing the latest version of an action method is usually easy to do by interacting with the sender control or gesture recognizer in the application. So for action methods, it is usually very easy to fine-tune them, or even write them from scratch while the application is running.

Conclusion

We have reached the end of this mastering live-coding series and you should now have a fairly good understanding of the live-coding in CodeFlow, good enough, I hope, to make it really easy for you to live-code all your applications in the future.

Post a Comment