Creating macOS Desktop Apps

This guide outlines the features of Solar2D that are specific to macOS desktop apps. A Corona-built macOS desktop app consists of a .app bundle which contains the Corona runtime engine and your Corona project’s compiled code and assets.

System Requirements

Note

All features supported by the Corona Simulator for macOS are supported for macOS desktop builds.

Building Great macOS Apps

Many developers will start with an iOS app, intending to duplicate it on the Mac platform. First and foremost, avoid the tempting assumption that all you need to do is choose FileBuildmacOS… from the Corona Simulator menu to create a macOS app from an iOS app. The desktop and device platforms are quite different, and while Corona hides many of the practical implementation details, it can’t magically turn an app designed for a touch screen device into a great mouse-based desktop experience.

Keep in mind that your macOS app will undergo the Apple review process before being accepted into the Mac App Store. This will cover the same type of issues as iOS app reviews, but there is an additional bar which must be cleared since macOS apps may be rejected on this point:

Your app appears to be a mostly unmodified port of an iOS app.

Among other things, you should consider how your app reacts to window size changes and how it works in full screen mode. The size of the content area is much more variable on the desktop than it is on devices and you will almost certainly need to make changes to your app so that it works well in the desktop environment. Remember that you can adjust both the window size and the content area and that they don’t necessarily need to be the same. Also remember that landscape orientation is what users will expect, so it will be more difficult to make a good impression with a portrait-oriented app in the desktop environment.

Obviously, anything you can download from the Mac App Store has passed the review process, so be sure to download other apps in the same genre as yours, see how they work on the desktop, and consider how you can modify yours to work similarly.

Window Settings

For both macOS desktop apps and Win32 desktop apps, Corona’s build.settings file supports a window table for customizing the app’s desktop window, including the default width/height, the title of the window, and more.

settings =
{
    window =
    {
        -- Settings for the desktop window; applies to both macOS and Win32 desktop apps
    },
}

Within the window table, the following settings are supported:

defaultMode

Sets how the window should be launched on startup. Supported values include "normal", "minimized", "maximized", or "fullscreen". Default is "normal". This can also be set programmatically via the native.setProperty() API.

settings =
{
    window =
    {
        defaultMode = "fullscreen",
    },
}
defaultViewWidth

Sets the default launch width of the view/client area of the window. This is the region within the borders of the window to which Corona renders. Ideally, this should match or exceed your config.lua content area width.

settings =
{
    window =
    {
        defaultViewWidth = 640,
    },
}
defaultViewHeight

Sets the default launch height of the view/client area of the window. This is the region within the borders of the window to which Corona renders. Ideally, this should match or exceed your config.lua content area height.

settings =
{
    window =
    {
        defaultViewHeight = 960,
    },
}
resizable

Set this to true to allow the end user to resize the window (the window is not resizable by default). Note that if true, you may need to handle Corona’s resize event to re-layout your content.

settings =
{
    window =
    {
        resizable = true,
    },
}
minViewWidth

This setting only applies if resizable is set to true. Prevents the user from resizing the window to a width smaller than this value. Note that this represents the width of the region within the borders of the window. If resizable is set to true and this setting is not specified, the window can be resized down to whatever width the OS allows.

settings =
{
    window =
    {
        minViewWidth = 320,
    },
}
minViewHeight

This setting only applies if resizable is set to true. Prevents the user from resizing the window to a height smaller than this value. Note that this represents the height of the region within the borders of the window. If resizable is set to true and this setting is not specified, the window can be resized down to whatever height the OS allows.

settings =
{
    window =
    {
        minViewHeight = 480,
    },
}
enableCloseButton

Enables or disables the window’s “close” button (enabled by default). If disabled, you must close the window in Lua via native.requestExit(). Note that Corona does not currently trigger an event when the “close” button is clicked.

settings =
{
    window =
    {
        enableCloseButton = true,
    },
}
enableMinimizeButton

Enables or disables the window’s “minimize” button (enabled by default).

settings =
{
    window =
    {
        enableMinimizeButton = true,
    },
}
enableMaximizeButton

Enables or disables the window’s “maximize” button (disabled by default). Note that the window will be resized when maximized/restored, so if this setting is true, you may need to handle Corona’s resize event to re-layout your content.

settings =
{
    window =
    {
        enableMaximizeButton = true,
    },
}
suspendWhenMinimized

Tells the runtime to suspend when the window is minimized (disabled by default).

settings =
{
    window =
    {
        suspendWhenMinimized = true,
    },
}
showWindowTitle

Causes the window’s title bar to be shown (by default) or hidden making the app’s content fill the entire window. Dragging at the top of the window will move it as with a regular window and clicks in this area do not go to the app. This setting is supported on macOS only.

settings =
{
    window =
    {
        showWindowTitle = false,
    },
}
titleText

Sets the window’s title bar text to the specified string (no title bar text by default). Supports 2-letter ISO 639‐1 language codes and optional 2-letter ISO 3166‐1 country codes (neither is case sensitive). The default title text can also be set programmatically via the native.setProperty() API.

settings =
{
    window =
    {
        titleText = {
            -- The "default" text will be used if the system is using a language and/or
            -- country code not defined below. This serves as a fallback mechanism.
            default = "Window Title Test",
            -- This text is used on English language systems in the United States.
            -- Note that the country code must be separated by a dash (-).
            ["en‐us"] = "Window Title Test (English‐USA)",
            -- This text is used on English language systems in the United Kingdom.
            -- Note that the country code must be separated by a dash (-).
            ["en‐gb"] = "Window Title Test (English‐UK)",
            -- This text is used for all other English language systems.
            ["en"] = "Window Title Test (English)",
            -- This text is used for all French language systems.
            ["fr"] = "Window Title Test (French)",
            -- This text is used for all Spanish language systems.
            ["es"] = "Window Title Test (Spanish)",
        },
    },
}

Excluding Files

You can exclude file(s) that aren’t needed for an macOS desktop app via the build.settings file’s excludeFiles pattern matching feature. Please see the Excluding Files section of the Project Build Settings guide for more details on pattern matching.

Including Resources

Sometimes it’s necessary to include certain files in your application to support specific macOS features. One example of this is localization which is achieved at the OS level (for example the Finder’s label for an app) using .lproj directories in the application’s Resource directory (background) page. Note that this mechanism to include arbitrary resources in your app is only intended to enable the knowledge to complete certain tasks and is not intended to automate anything.

To include arbitrary files in your app’s Resource directory add a bundleResourcesDirectory entry to your project’s build.settings for macOS builds. For example:

settings =
{
    macos =
    {
        bundleResourcesDirectory = "osx-resources",
    },
}

Then, in the project, the contents of the osx-resources directory might look like this:

fr.lproj/

osx-resources/fr.lproj:
InfoPlist.strings

This results in the fr.lproj directory ending up in MyApp.app/Content/Resources/fr.lproj.

You will probably want to add the directory you use as bundleResourcesDirectory to your excluded files.

Custom URL Schemes

To enable a custom URL scheme in your macOS app, you’ll need to set it up in the app’s Info.plist by including a section like this in build.settings:

settings =
{
    macos =
    {
        plist =
        {
            CFBundleURLTypes =
            {
                {
                    CFBundleURLName = "My URL Scheme",
                    CFBundleURLSchemes = {
                        "myscheme",
                    },
                },
            },
        },
    },
}

This will allow the app to receive a message from (or be started by) a Terminal command such as:

open myscheme://these/are/the/parameters

To handle the message in your Corona app, implement a handler like this:

local function onSystemEvent( event )
    if ( event.type == "applicationOpen" and event.url ) then
        local launchURL = event.url
        print( "Event: applicationOpen - launchURL: ", launchURL )
    end
end

Runtime:addEventListener( "system", onSystemEvent )

Custom Document Types

Similarly to how custom URL schemes work you can set up document types that you app recognizes and which can be opened by dragging them to the app’s icon in the Finder or the Dock.

To enable custom document types in your macOS app, you’ll need to set it up in the app’s Info.plist by including a section like this in build.settings:

settings =
{
    macos =
    {
        plist =
        {
            CFBundleDocumentTypes =
            {
                {
                    CFBundleTypeExtensions =
                    {
                        "png",
                    },
                    CFBundleTypeIconFile = "app.icns",
                    CFBundleTypeName = "public.png",
                    LSHandlerRank = "Alternate",
                    LSItemContentTypes =
                    {
                        "public.png",
                    },
                },
            },
        },
    },
}

In your Corona app, implement a handler like this:

local function onSystemEvent( event )
    if ( event.type == "applicationOpen" and event.url ) then
        local filename = event.url
        print( "Event: applicationOpen - filename: ", filename )
    end
end

Runtime:addEventListener( "system", onSystemEvent )

This code will called when files with the configured types are dropped on your app’s icon in the Finder or the Dock. More information is available in Apple’s developer Core Foundation Keys documentation.

Entitlements

If you need custom entitlements in your macOS app, specify them in build.settings. For example, you might need to specify the com.apple.security.personal-information.location entitlement if you use Corona’s location features.

settings =
{
    macos =
    {
        entitlements = {
            ["com.apple.security.personal-information.location"] = true,
        },
    },
}

App Icon

You should add a Icon-osx.icns file to your Corona project’s root directory to provide an icon for your application on macOS This should be an .icns file which contains multiple versions of your icon at different resolutions (details). This Icon-osx.icns file will be used to set the icon for the following:

  1. The icon that your .app bundle uses, as viewed in the Finder.
  2. The icon used by your app in the Dock.

You can find apps in the Mac App Store to help with the creation of .icns files.

Building / Running

To build and run your macOS desktop app, follow these steps:

  1. Open the Corona Simulator.

  2. Open and run a Corona project.

  3. Select the FileBuildmacOS… menu item to reveal the macOS build dialog.

  4. The Application Name, Version, and Save to Folder fields are all required. Here are notes regarding these build dialog fields:

    • Application Name — The application name you’ve entered can be fetched at runtime via Corona’s system.getInfo( "appName" ) call (reference).

    • Version — The version string you’ve entered can be fetched at runtime via Corona’s system.getInfo( "appVersionString" ) call (reference).

    • Provisioning Profile — The provisioning profile for the app; see App Signing below.

    • Save to Folder — The directory in which to save the built project.

    • After Build — select which action should be performed after the app is successfully built.

  5. Click the Build button to build your application to the given Save to Folder location. If you chose Send to Mac App Store…, a panel will open which allows you to select the tab for the preferred option and follow the prompts to complete your build.

Debugging

The Corona Simulator does not currently support direct simulation of an app running in macOS desktop mode, although the Simulator running a “Custom Device” skin is almost the same.

Note that when running a built macOS Corona app or the Corona Shell, all print() output and Lua errors/warnings will be streamed to stdout. There are various ways to manage this output. One way is to run the executable embedded in the app bundle in a Terminal window which will make the debug output visible. For example:

./Bridge\ for\ OS\ X/Contents/MacOS/Bridge\ for\ OS\ X

In this example, the backslashes (\) protect the spaces in the filename (you could alternatively use quotes).

You can also use the Console app in the ApplicationsUtilities folder to view application output.

If you run the app by choosing the After BuildOpen application option in the Corona Simulator build dialog, the app’s output will appear in the Corona Simulator Console window.

App Signing

You’ll need a “Mac Apps” provisioning profile from the Apple Developer portal in order to sign your macOS app. This works very similarly to the iOS app signing process, but you may optionally choose to create an unsigned app by selecting None in the Provisioning Profile dropdown when performing a build. Unsigned apps can be run on your own machine and, with some tweaking of the “Security & Privacy” settings within “System Preferences,” on other Macs. However, unsigned apps can not be submitted to the Mac App Store, so you will eventually need to sort out your Mac provisioning profiles.

Provisioning profiles should be installed in ~/Library/MobileDevice/Provisioning Profiles. It may be necessary to do this manually.

For testing, it’s recommended you sign the app with your Mac App signing identity by choosing it in the Corona Simulator Build for macOS dialog and then tell your testers to choose the Mac App Store and identified developers option on the “Security & Privacy” panel of “System Preferences”.

Distribution

You can distribute your app via the Mac App Store, a .dmg, or any other way you may distribute an .app bundle.

If not using the Mac App Store, we recommend that you sign the app with your Mac App signing identity by choosing it in the Corona Simulator build dialog menu and then tell your testers to choose the Mac App Store and identified developers option on the “Security & Privacy” panel of “System Preferences”.

If using a .zip archive to distribute your app, be aware that there are symbolic links in the built .app bundle, so you must specify the -y option if running zip on the command line to create your archive (using the Compress option in the Finder does the correct thing automatically). Failure to do this will result in macOS saying the app is damaged when run on another computer. Also note that some file sharing services can corrupt macOS applications, so we recommend that you put them in .zip archives before uploading them.