This tutorial will help you get started with Solar2D Native builds for iOS.
Before you begin, we recommend that you update to the latest version of Xcode.
To set up Solar2D Native, follow these steps:
Once you have Corona installed, locate its core application folder. Open the Native folder within and run the Setup Corona Native application.
Solar2D Native projects should generally use the provided App
template as a foundation, so copy the App
folder /Native/Project Template/App
)
Once copied, rename the folder to whatever you want. In this tutorial, we’ll build a sample plugin which activates/deactivates the device’s “flashlight” and then we’ll integrate it into a test app. Thus, Flashlight
is an appropriate folder name.
Let’s get started! First, we’ll address the plugin side of the project:
Open your Flashlight
folder — a copy of the App
template as described above — and then open the ios
folder.
Inside this ios
folder you’ll see several files, two of which should be noted at this point:
App.xcodeproj
Plugin.xcodeproj
Basically, these are Xcode Project files which contain various project files and settings.
Plugin.xcodeproj
file to load the project in Xcode. Once loaded, inspect the If the Navigator is not visible, reveal it via
PluginLibrary.h
and PluginLibrary.mm
files — these are the files which we’ll modify to build the plugin.For those with a background in C programming, the .mm
extension may seem a bit odd. In Xcode, a .m
file is really a .c
file, but it stands for Methods. This .mm
file allows Xcode to mix .h
file, this is the traditional C “header” file where you’ll place the definitions for the objects.
Now let’s proceed with modifying the methods file (PluginLibrary.mm
):
Within Xcode, select PluginLibrary.mm
from the
For this tutorial, the plugin will need access to the AVFoundation
framework. As such, we must import the appropriate header file just after the UIKit
import by adding the indicated highlighted line:
#import "PluginLibrary.h" #include <CoronaRuntime.h> #import <UIKit/UIKit.h> #import <AVFoundation/AVFoundation.h>
PluginLibrary
class is declared. This is the interface for binding your plugin to Lua:class PluginLibrary { public: typedef PluginLibrary Self; public: static const char kName[]; static const char kEvent[]; protected: PluginLibrary(); public: bool Initialize( CoronaLuaRef listener ); public: CoronaLuaRef GetListener() const { return fListener; } public: static int Open( lua_State *L ); protected: static int Finalizer( lua_State *L ); public: static Self *ToLibrary( lua_State *L ); public: static int init( lua_State *L ); static int show( lua_State *L ); private: CoronaLuaRef fListener; };
Most of this requires no modification except for the final public:
block where two methods are defined:
public: static int init( lua_State *L ); static int show( lua_State *L ); private: CoronaLuaRef fListener; };
For our flashlight plugin, show()
doesn’t make much semantic sense. Instead, methods of on()
and off()
are more logical since the plugin will turn the flashlight on or off. Thus, let’s remove the show()
method and add both an on()
and off()
method instead:
public: static int init( lua_State *L ); static int on( lua_State *L ); static int off( lua_State *L ); private: CoronaLuaRef fListener; };
"plugin.library"
to "plugin.flashlight"
and, a few lines after, "pluginlibraryevent"
to "Flashlight"
:// This corresponds to the name of the library, e.g. [Lua] require "plugin.library" const char PluginLibrary::kName[] = "plugin.flashlight"; // This corresponds to the event name, e.g. [Lua] event.name const char PluginLibrary::kEvent[] = "Flashlight";
PluginLibrary::Open()
method needs updating to remove the show()
method and add the on()
and off()
methods we specified earlier:int PluginLibrary::Open( lua_State *L ) { // Register __gc callback const char kMetatableName[] = __FILE__; // Globally unique string to prevent collision CoronaLuaInitializeGCMetatable( L, kMetatableName, Finalizer ); // Functions in library const luaL_Reg kVTable[] = { { "init", init }, { "on", on }, { "off", off }, { NULL, NULL } }; // Set library as upvalue for each library function Self *library = new Self; CoronaLuaPushUserdata( L, library, kMetatableName ); luaL_openlib( L, kName, kVTable, 1 ); // leave "library" on top of stack return 1; }
show()
method. Since we don’t need this method for our flashlight plugin, remove it and replace it with two new methods for on()
and off()
:// [Lua] library.on( word ) int PluginLibrary::on( lua_State *L ) { NSString *message = @"Device does not appear to have a camera light."; // check if flashlight available Class captureDeviceClass = NSClassFromString(@"AVCaptureDevice"); if (captureDeviceClass != nil) { AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; if ([device hasTorch] && [device hasFlash]){ message = @"Device light should be on."; [device lockForConfiguration:nil]; if (device.torchMode == AVCaptureTorchModeOff) { [device setTorchMode:AVCaptureTorchModeOn]; [device setFlashMode:AVCaptureFlashModeOn]; } [device unlockForConfiguration]; } } Self *library = ToLibrary( L ); // Create event and add message to it CoronaLuaNewEvent( L, kEvent ); lua_pushstring( L, [message UTF8String] ); lua_setfield( L, -2, "message" ); // Dispatch event to library listener CoronaLuaDispatchEvent( L, library->GetListener(), 0 ); return 0; } // [Lua] library.off( word ) int PluginLibrary::off( lua_State *L ) { NSString *message = @"Device does not appear to have a camera light."; ; // check if flashlight available Class captureDeviceClass = NSClassFromString(@"AVCaptureDevice"); if (captureDeviceClass != nil) { AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; if ([device hasTorch] && [device hasFlash]){ message = @"Device light should be off."; [device lockForConfiguration:nil]; if (device.torchMode == AVCaptureTorchModeOn) { [device setTorchMode:AVCaptureTorchModeOff]; [device setFlashMode:AVCaptureFlashModeOff]; } [device unlockForConfiguration]; } } Self *library = ToLibrary( L ); // Create event and add message to it CoronaLuaNewEvent( L, kEvent ); lua_pushstring( L, [message UTF8String] ); lua_setfield( L, -2, "message" ); // Dispatch event to library listener CoronaLuaDispatchEvent( L, library->GetListener(), 0 ); return 0; }
Within each new method, following the
Self *library = ToLibrary( L ); // Create event and add message to it CoronaLuaNewEvent( L, kEvent ); lua_pushstring( L, [message UTF8String] ); lua_setfield( L, -2, "message" ); // Dispatch event to library listener CoronaLuaDispatchEvent( L, library->GetListener(), 0 ); return 0; }
Basically, this code creates a C object called library
which is the pathway to the Lua app. Next, it calls CoronaLuaNewEvent()
and passes it the kEvent
object. It then adds the string message
and tells Lua that it’s the second entry in the table. Finally, it dispatches the event to the library (further on in this tutorial we’ll discuss the Lua code for handling this event).
PluginLibrary.mm
file is this:CORONA_EXPORT int luaopen_plugin_library( lua_State *L ) { return PluginLibrary::Open( L ); }
Because of the “suffix” of _library
, the name of the plugin on the Corona side will be library
. That name is vague if you decide to build several plugins, so let’s rename the plugin flashlight
by changing the code to luaopen_plugin_flashlight
:
CORONA_EXPORT int luaopen_plugin_flashlight( lua_State *L ) { return PluginLibrary::Open( L ); }
PluginLibrary.mm
file and proceed to the next section.Now let’s proceed with modifying the header file (PluginLibrary.h
):
Within Xcode, select PluginLibrary.h
from the
Change the CORONA_EXPORT
line to the following:
// This corresponds to the name of the library, e.g. [Lua] require "plugin.library" // where the '.' is replaced with '_' CORONA_EXPORT int luaopen_plugin_flashlight( lua_State *L ); #endif // _PluginLibrary_H__
PluginLibrary.h
file and proceed to the next section.Xcode projects need to be built to verify the code and test for any errors. Confirm that you saved all changes to both PluginLibrary.mm
and PluginLibrary.h
, then select
Now, from within your core Flashlight
folder, open the App.xcodeproj
project and proceed with the following steps:
App
) in the App
to Flashlight
. Upon entry, Xcode will analyze the project and show a dialog box indicating all of the names it will change. Click Rename to confirm.AVFoundation.framework
. If you do not see it, click the [+] button below the list, select AVFoundation.framework
, and click Add.The final step is to implement our plugin into a standard Corona (Lua) project. Within Xcode, from the
Here you’ll see typical Corona project files like main.lua
, build.settings
, and config.lua
. For Solar2D Native, the latter two files are typically not used, so we can ignore them for this tutorial.
Select main.lua
to reveal its contents (or alternatively open the file in your preferred text editor) and then replace the existing file code with the following:
local flashlight = require( "plugin.flashlight" ) local widget = require( "widget" ) -- This event is dispatched to the global Runtime object by "didLoadMain:" in MyCoronaDelegate.mm local function delegateListener( event ) native.showAlert( "Event dispatched from 'didLoadMain:'", "of type: " .. tostring( event.name ), { "OK" } ) end Runtime:addEventListener( "delegate", delegateListener ) local function listener( event ) print( "Received event from Flashlight plugin (" .. event.name .. "): ", event.message ) end local lightState = "off" local function handleButtonEvent( event ) if ( lightState == "off" ) then flashlight.on() lightState = "on" event.target:setLabel( "Turn Off" ) else flashlight.off() lightState = "off" event.target:setLabel( "Turn On" ) end return true end local onOffSwitch = widget.newButton( { x = display.contentCenterX, y = display.contentCenterY, label = "Turn On", onRelease = handleButtonEvent }) flashlight.init( listener )
Let’s inspect this code step by step:
The first line loads our plugin into a Corona variable called flashlight
, similar to how you’d include any typical Corona plugin. The second line includes the Corona widget library which we’ll use to create a standard button.
Next is the function called delegateListener()
. This function will be attached to the Runtime event "delegate"
and it will receive events dispatched from the native side. Basically, this lets you know when the app has been loaded, but this sample won’t use it in any meaningful way (in this case, it just shows an alert).
The next function is the listener that the app uses to receive events from various plugin methods. Like the previous function, this won’t be used in any meaningful way within this sample.
The rest of the code does the main work from the Corona side. Among other things, we create a widget button that uses the listener function named handleButtonEvent()
. Most importantly, notice that the handleButtonEvent()
function calls flashlight.on()
and flashlight.off()
, the functions that we wrote in PluginLibrary.mm
.
local function handleButtonEvent( event ) if ( lightState == "off" ) then flashlight.on() lightState = "on" event.target:setLabel( "Turn Off" ) else flashlight.off() lightState = "off" event.target:setLabel( "Turn On" ) end return true end
And now for the fun — testing on an actual device!
Assuming you’ve enabled your iOS device as a development device, connect it to your computer with the sync cable.
In the Xcode tool bar, click on the active scheme button and confirm that App is the selected scheme:
To the left of the scheme button, you’ll see a Run button. Click this and Xcode will install the app onto the device and start it. Because it’s in development mode, it may take a couple seconds longer to start up.
If everything is working correctly, you’ll see a “Turn On” button in the center of the screen. Tap it and the device’s flashlight should turn on. Tap the button again to turn off the flashlight.
When you’re finished testing, click the Stop button in Xcode.
Clearly this tutorial is just a basic example of what can be done with Solar2D Native, but hopefully it has shown you what can be accomplished when you combine the nearly unlimited power of native programming with the ease and simplicity of Corona!