Using Python With Swift 3
(TL;DR: Create a python .plugin bundle with py2app
, load it in main.swift
and use @objc
protocols to specify concrete interfaces for the python classes
to implement. Example here.)
(This is an update to an earlier article for compatability with Swift 3 and XCode 8.1. There has been a couple of name and interface changes, but nothing major.)
I wanted to use an existing library of python code that I had written, in a new macOS application - to provide an easy-to-use UI for the library. I wanted to use Swift, both because I find it an infinitely nicer language than Objective-C, and it seems Swift is now accepted as mainstream.
There is lots of information around on how to integrate python with Objective-C
via the pyobjc
python library, but a lot of information is very old
and I couldn’t find anything that discussed or was even as recent as Swift.
The best way to create executable bundles with python is with
py2app
, which has two modes of operation - creating an executable
application .app
bundle with the main executable written in python and
calling into compiled swift code, and creating a python-based .plugin
bundle
that is loaded by the Swift-based application. After having various issues
working with the .app
method, I decided to use the .plugin
approach, despite
the relative lack of documentation.
This article shows a very basic application to demonstrate the fundamental principals of integrating swift and python.
The aims for the application are:
- A canonical storyboard-based swift app
- A single window that has a custom NSView
- The view controller for the window is written in swift and calls python code in order to get the python version, and prints it to the console
- The NSView is a python-based custom view with a colored background
And a completed version of the application developed in this article can be found here.
Basic Integration
Let’s start by building the very basic infrastructure; a python-based plugin that is loaded by our swift application, and simply prints to standard output to let us know that it has been executed.
1. Create the XCode project
Start by creating a new macOS Cococa Application project in XCode, and set the language to Swift.
I’m calling the application PythonToSwiftExample
, which doesn’t matter except
that it changes the default namespace by which your swift interfaces are exported.
We’re also using storyboards.
There are just a couple of changes to the project at this stage. In order to
allow the storyboards or XIB files to reference python classes (e.g. custom
views or controllers), the python classes need to be created before the
interface is. Loading of the primary storyboard or XIB is done inside the
NSApplicationMain
function, before any user-created code is called. We
therefore need to override the default implementation of the main()
entry
point.
Firstly, go to the generated AppDelegate.swift
application delegate source,
and remove the class attribute that says @NSApplicationMain
from the delegate.
This will prevent the default entry from being synthesized.
Secondly, add a new Swift file to the project, and name it main.swift
. The
name is important, because the first executable line in a swift file with that
name is used as the first piece of code run upon starting the executable. For
now, we just want to replicate the default behavior, so the contents of
main.swift
are:
import Cocoa
exit(NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv))
2. Create the python .plugin bundle
Now let’s create the basic python infrastructure. In the terminal, navigate to
the project root folder you just created. Decide on which python version you
want to use for the plugin - python3 via pyenv
is used in this
tutorial, but setting up your python environment is beyond the scope of this
article. Make sure that the python packages pyobjc
and py2app
are
installed in your environment:
$ pip3 install pyobjc py2app
Create a simple placeholder for the entry point into the bundle, that we
call here Bridge.py
:
"""Bridge.py. The main Python-(Swift) plugin bundle entry module"""
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
logger.info("Loaded python bundle")
And create the setuptools
/py2app
setup file, setup.py
to load , with the contents:
"""Setuptools setup for creating a .plugin bundle"""
from setuptools import setup
APP = ['Bridge.py']
OPTIONS = {
# Any local packages to include in the bundle should go here.
# See the py2app documentation for more
"includes": [],
}
setup(
plugin=APP,
options={'py2app': OPTIONS},
setup_requires=['py2app'],
install_requires=['pyobjc'],
)
This will allow py2app
to build a plugin that loads Bridge.py
, and that
contains the additional package bridge
, copied into the bundle.
Now let’s create the .plugin
bundle that will be loaded into the
application. Although we are far from useful on the python side, the -A
argument will only create an alias to the code location, so that the bundle
does not have to be rebuilt every time the python code changes. Of course, this
is for development only, not distribution.
$ python setup.py py2app -A
running py2app
creating /Users/xgkkp/tmp/PythonToSwiftExample/build
creating /Users/xgkkp/tmp/PythonToSwiftExample/build/bdist.macosx-10.11-x86_64
creating /Users/xgkkp/tmp/PythonToSwiftExample/build/bdist.macosx-10.11-x86_64/python3.5-standalone
creating /Users/xgkkp/tmp/PythonToSwiftExample/build/bdist.macosx-10.11-x86_64/python3.5-standalone/app
creating /Users/xgkkp/tmp/PythonToSwiftExample/build/bdist.macosx-10.11-x86_64/python3.5-standalone/app/collect
creating /Users/xgkkp/tmp/PythonToSwiftExample/build/bdist.macosx-10.11-x86_64/python3.5-standalone/app/temp
creating /Users/xgkkp/tmp/PythonToSwiftExample/dist
creating build/bdist.macosx-10.11-x86_64/python3.5-standalone/app/lib-dynload
creating build/bdist.macosx-10.11-x86_64/python3.5-standalone/app/Frameworks
*** creating plugin bundle: Bridge ***
Done!
We now have a working, active .plugin
bundle, in dist/Bridge.plugin
. Let’s
now integrate this into our application. Back to XCode.
3. Loading the .plugin
In XCode, go to File -> Add Files to "PythonToSwiftExample"...
. Browse to the
PROJECT_ROOT/dist
folder, select Bridge.plugin
and click add. The plugin will
now be copied into the application bundle when you build inside XCode.
Now let’s actually load the plugin. Alter the main.swift
entry point file so
that it locates and loads the plugin bundle:
// Application main entry point
import Cocoa
let path = Bundle.main.path(forResource: "Bridge", ofType: "plugin")
guard let pluginbundle = Bundle(path: path!) else {
fatalError("Could not load python plugin bundle")
}
pluginbundle.load()
let ret = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
exit(ret)
And now we can run the application! If everything is working correctly, then a blank window should open and the python logging output should be visible in the XCode output window:
More Useful Behaviour: Integration
Now we have the very basic infrastructure, let’s actually write code that
communicates between swift and python. With pyobjc
, a python class needs to
inherit from NSObject
in order to be visible to Objective-C. Using Swift,
any objects passed to python need to inherit from an Objective-C class, or
annotated with the @objc
attribute - this ensures that they are created and
passed as Objective-C objects. For more details, see the Swift
attributes documentation.
We will be communicating with python in a couple of ways:
- Create the NSObject-derived class in python, and load dynamically in Swift
with
NSClassFromString
. This is effectively what happens when using named python-based classes in the storyboards and XIBs. - Take advantage of the bundles principal class to be handed a class with an expected interface.
In order to use the principal class we need to know the interface it adheres to. Since this is a custom interface, it is unlikely that any existing protocol will be suitable, and with Swift we can’t just rely on knowing we are sending the right messages. So, let’s create an Objective-C protocol, subclass from it in python, and then instantiate it in Swift.
Loading the .plugin Principal class
Create a new Swift file in the XCode project, named BridgeInterface.swift
:
import Foundation
/// A simple demonstration interface to the python module
@objc public protocol BridgeInterface {
static func createInstance() -> BridgeInterface
func getPythonInformation() -> String
}
/// A simple class for access to an instance of the python interface
class Bridge {
static private var instance : BridgeInterface?
static func sharedInstance() -> BridgeInterface {
return instance!
}
static func setSharedInstance(to: BridgeInterface?) {
instance = to
}
}
This gives us an Objective-C protocol, BridgeInterface
with a class method
to instantiate itself (this works because the method implementation is
dynamically dispatched to the Type
reference we will have shortly), and a
simple instance method getPythonInformation
that will just return a
String
. We also have a convenience accessor class - Bridge
. This lets us
worry about how to get hold of our initial instance of BridgeInterface
only
once, and the rest of the program can use the convenience class afterwards.
Let’s see this in practice; edit main.swift
to put this after the call to
pluginbundle.load()
and before the call to NSApplicationMain
:
// Load the principal class
guard let pc = pluginbundle.principalClass as? BridgeInterface.Type else {
fatalError("Could not load principal class from python bundle")
}
// Create an instance of the principal class and store it
let interface = pc.createInstance()
Bridge.setSharedInstance(to: interface)
Bridge.sharedInstance().
And, to prove that we can now use this in the main application, go to the
autogenerated ViewController.swift
and add the following lines into the
viewDidLoad
function:
let pythonMessage = Bridge.sharedInstance().getPythonInformation()
Swift.print("Info from python:\n\(pythonMessage)")
Creating the Principal Class
Running at this point will simply get the error
fatal error: Could not load principal class from python bundle
because we have not yet create the class in python! Although py2app
lets you
customize the principal class (via the bundles Info.plist
) the default is
a class with the same name as the bundle, which in this case would be Bridge
.
Let’s look at the lines we are adding to Bridge.py
(since we only had logging
before, it shouldn’t matter if you instead replace the contents):
import sys
import objc
from Foundation import NSObject
# Load the protocol from Objective-C
BridgeInterface = objc.protocolNamed("PythonToSwiftExample.BridgeInterface")
class Bridge(NSObject, protocols=[BridgeInterface]):
@classmethod
def createInstance(self):
return Bridge.alloc().init()
def getPythonInformation(self):
return sys.version
The first important part here is loading the protocol; Because we are using a swift application bundle, the protocol is prefixed by the Swift modules name (whereas with plain Objective-C this might not be the case).
Secondly is the way in which we declare our Bridge
class - due to the nature
of the pyobjc
bridge, inheriting from a protocol does not correctly create
an Objective-C dynamic NSObject. The syntax displayed here is only valid
for Python 3 upwards - see the pyobjc
documentation for older methods of
declaring protocol conformance.
Now, running the swift application should work - loading a blank window, and spitting out the python version information into the output log.
Custom NSView in python
Lastly, let’s demonstrated creating a custom NSView that simply colours it’s
background. In XCode, open Main.storyboard
. Drag out a “Custom View” from
the toolbox to the main view controller, go to the properties tab and change
the “Custom Class”-“Class” field to read ColouredView
.
Now go back to Bridge.py
. Add the following imports to the module:
from Cocoa import NSView
from AppKit import NSGraphicsContext, NSRectToCGRect
import Quartz
and add the following class declaration; we are creating an NSView subclass
that overrides the drawRect:
selector. The underscore is required, as per
the pyobjc
naming rules, but otherwise the calls should be
relatively familiar:
class ColouredView(NSView):
def drawRect_(self, dirtyRect):
context = NSGraphicsContext.currentContext().CGContext()
Quartz.CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0)
Quartz.CGContextFillRect(context, dirtyRect)
and running this gives the expected, complete output: