Pyobjus on iOS

You may be wondering how to run pyobjus on iOS devices? The solution to this problem is to use kivy-ios.

As you can see, kivy-ios contains scripts for building kivy, pyobjus and other libraries needed for running your app. It also provides scripts for making xcode projects from which you can run your python/kivy/pyobjus applications. Sounds great, and it is.

Example with Kivy UI

Let’s first build kivy-ios. Execute following command:

git clone https://github.com/kivy/kivy-ios.git
cd kivy-ios
./toolchain.py build kivy pyobjus

This can take some time.

You can build your UI with the kivy framework, and access device hardware using pyobjus. So, let’s look at one simple example of this. Notice that a tutorial describing how to use kivy-ios exists as part of the official kivy-ios documentation, but here we will provide another one, with focus on pyobjus.

Let’s first make one simple example of using pyobjus with kivy.:

from pyobjus import autoclass, objc_f, objc_str
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.graphics import Color, Ellipse, Line

NSArray = autoclass('NSArray')
array = NSArray.arrayWithObjects_(objc_f(0.3), objc_f(1), objc_f(1), None)

class MyPaintWidget(Widget):

    def on_touch_down(self, touch):
        color = (array.objectAtIndex_(0).floatValue(), array.objectAtIndex_(1).floatValue(), array.objectAtIndex_(2).floatValue())
        with self.canvas:
            Color(*color, mode='hsv')
            d = 30.
            Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
            touch.ud['line'] = Line(points=(touch.x, touch.y))

    def on_touch_move(self, touch):
        touch.ud['line'].points += [touch.x, touch.y]


class MyPaintApp(App):

    def build(self):
        parent = Widget()
        painter = MyPaintWidget()
        btn_text = objc_str('Clear')
        clearbtn = Button(text=btn_text.UTF8String())
        parent.add_widget(painter)
        parent.add_widget(clearbtn)

        def clear_canvas(obj):
            painter.canvas.clear()
        clearbtn.bind(on_release=clear_canvas)

        return parent

if __name__ == '__main__':
    MyPaintApp().run()

Please save this code inside a file with the name main.py. You will need to make a directory which will hold your python application code. For example, you can do the following:

mkdir ~/paint
mv main.py ~/paint

So now paint contains main.py file which holds your python code.

The example above is borrowed from this tutorial but we have added some pyobjus things to it. So we are now using a NSArray to store information about the line color, and we are using a NSString to set the text of the button.

Now you can create an xcode project which will hold our python application. kivy-ios comes with script for creating xcode projects for you. You only need to specify the project name and the absolute path to your app.

Execute the following command:

./toolchain.py create paintApp ~/paint/

Notice the following. The second parameter which we are passing to the script is the name of our app. In this case, the name of our iOS app will be paintApp. The third parameter is the absolute path to our python app which we want to run on iOS.

After executing this command you will get output similar to this:

-> Create /Users/myName/kivy-ios/paintApp-ios directory
-> Copy templates
-> Customize templates
-> Done !

Your project is available at /Users/myName/kivy-ios/paintapp-ios

You can now type: open /Users/myName/kivy-ios/paintapp-ios/paintapp.xcodeproj

Note that the name is converted to lower case. If you enter the paintapp-ios directory you will see that there are main.m, bridge.m and other resources.

You can open this project with xcode as follows:

open /Users/myName/kivy-ios/paintapp-ios/paintapp.xcodeproj

If you have setup your developer account, you only need to click play and the app will be deployed on your iOS device.

This is screenshoot from my iPad.

_images/IMG_0322.PNG

Accessing the accelerometer

To access the accelerometer on iOS devices, you use the CoreMotion framework. The CoreMotion framework is added by default in the project template which ships with kivy-ios.

Let’s say that we have a class interface with the following properties and variables:

@interface bridge : NSObject {
    NSOperationQueue *queue;
}

@property (strong, nonatomic) CMMotionManager *motionManager;
@property (nonatomic) double ac_x;
@property (nonatomic) double ac_y;
@property (nonatomic) double ac_z;
@end

Also, let’s say that we have an init method which inits the motionManager and the queue, and we have a method for running the accelerometer, and that method is declared as follows:

- (void)startAccelerometer {
    if ([self.motionManager isAccelerometerAvailable] == YES) {
        [self.motionManager startAccelerometerUpdatesToQueue:queue withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
            self.ac_x = accelerometerData.acceleration.x;
            self.ac_y = accelerometerData.acceleration.y;
            self.ac_z = accelerometerData.acceleration.z;
        }];
    }
}

You can see here that we are specifying a handler which will be called when we get some updates from the accelerometer. Currently you can’t implement this handler from pyobjus, so that may be a problem.

But, we have solution for this. We have added a bridge class for this purpose: to implement handlers in pure Objective C, and then call methods of the bridge class so we can get the actual data in Python. In this example, we are storing the x, y and z values from the accelerometer in the ac_x, ac_y and ac_z class properties. We can then easily access these class properties.

So let’s see a basic example how to read accelerometer data from pyobjus:

from pyobjus import autoclass

def run():
    Bridge = autoclass('bridge')
    br = Bridge.alloc().init()
    br.motionManager.setAccelerometerUpdateInterval_(0.1)
    br.startAccelerometer()

    for i in range(10000):
        print 'x: {0} y: {1} z: {2}'.format(br.ac_x, br.ac_y, br.ac_z)

if __name__ == "__main__":
    run()

So if you run this script on an iPad, in the way we have shown above, you’ll get output similar to this in the xcode console:

x: 0.0219268798828 y: 0.111801147461 z: -0.976440429688
x: 0.0219268798828 y: 0.111801147461 z: -0.976440429688
x: 0.0219268798828 y: 0.111801147461 z: -0.976440429688
x: 0.0219268798828 y: 0.111801147461 z: -0.964920043945
x: 0.145629882812 y: -0.00624084472656 z: -0.964920043945
x: 0.145629882812 y: -0.00624084472656 z: -0.964920043945
x: 0.145629882812 y: -0.00624084472656 z: -0.964920043945
x: 0.145629882812 y: -0.00624084472656 z: -0.964920043945

As you can see, we have data from the accelerometer, so you can use it for some practical purposes if you want.

Accessing the gyroscope

In a similar way as we accessed the accelerometer, we can access the gyroscope. So let’s expand our bridge class interface with properties which will hold gyro data:

@property (nonatomic) double gy_x;
@property (nonatomic) double gy_y;
@property (nonatomic) double gy_z;

Then in the bridge class implementation, add the following method:

- (void)startGyroscope {
    if ([self.motionManager isGyroAvailable] == YES) {
        [self.motionManager startGyroUpdatesToQueue:queue withHandler:^(CMGyroData *gyroData, NSError *error) {
            self.gy_x = gyroData.rotationRate.x;
            self.gy_y = gyroData.rotationRate.y;
            self.gy_z = gyroData.rotationRate.z;
        }];
    }
}

This method is probably familiar to you because it is very similar to the method used for getting accelerometer data. Let’s write some python code to read this data from python:

from pyobjus import autoclass

def run():
    Bridge = autoclass('bridge')
    br = Bridge.alloc().init()
    br.startGyroscope()

    for i in range(10000):
        print 'x: {0} y: {1} z: {2}'.format(br.gy_x, br.gy_y, br.gy_z)

if __name__ == "__main__":
    run()

You will get output similar to this:

x: 0.019542276079 y: 0.0267431973505 z: 0.00300590992237
x: 0.019542276079 y: 0.0267431973505 z: 0.00300590992237
x: 0.019542276079 y: 0.0267431973505 z: 0.00300590992237
x: 0.019542276079 y: 0.0267431973505 z: 0.00300590992237
x: 0.019542276079 y: 0.0267431973505 z: 0.00300590992237
x: 0.019542276079 y: 0.018291389315 z: -0.00338913880323
x: 0.018301243011 y: 0.018291389315 z: -0.00338913880323
x: 0.018301243011 y: 0.018291389315 z: -0.00338913880323
x: 0.018301243011 y: 0.018291389315 z: -0.00338913880323
x: 0.018301243011 y: 0.018291389315 z: -0.00338913880323
x: 0.018301243011 y: 0.018291389315 z: -0.00338913880323
x: 0.0183009766949 y: 0.0170807162834 z: -0.00339499775763
x: 0.0183009766949 y: 0.0170807162834 z: -0.00339499775763

So now you can use gyro data in your Python kivy application.

Accessing the magnetometer

You can probably guess that this will be almost identical to the previous two examples. Let’s add two new properties to the interface of the bridge class:

@property (nonatomic) double mg_x;
@property (nonatomic) double mg_y;
@property (nonatomic) double mg_z;

And then add the following method to the bridge class:

- (void)startMagnetometer {
    if (self.motionManager.magnetometerAvailable) {
        [self.motionManager startMagnetometerUpdatesToQueue:queue withHandler:^(CMMagnetometerData *magnetometerData, NSError *error) {
            self.mg_x = magnetometerData.magneticField.x;
            self.mg_y = magnetometerData.magneticField.y;
            self.mg_z = magnetometerData.magneticField.z;
        }];
    }
}

Now we can use the methods above from pyobjus to get the data from the magnetometer:

from pyobjus import autoclass

def run():
    Bridge = autoclass('bridge')
    br = Bridge.alloc().init()
    br.startMagnetometer()

    for i in range(10000):
        print 'x: {0} y: {1} z: {2}'.format(br.mg_x, br.mg_y, br.mg_z)

if __name__ == "__main__":
    run()

You will get output similar to this:

x: 29.109375 y: -46.694519043 z: -27.4476470947
x: 29.109375 y: -46.694519043 z: -27.4476470947
x: 29.109375 y: -47.7679595947 z: -24.6468658447
x: 28.03125 y: -47.7679595947 z: -24.6468658447
x: 28.03125 y: -47.7679595947 z: -24.6468658447
: 28.03125 y: -47.7679595947 z: -24.6468658447
x: 28.03125 y: -47.7679595947 z: -24.6468658447
x: 28.03125 y: -48.3046875 z: -27.4476470947
x: 27.4921875 y: -48.3046875 z: -27.4476470947
x: 27.4921875 y: -48.3046875 z: -27.4476470947
x: 27.4921875 y: -48.3046875 z: -27.4476470947
x: 27.4921875 y: -48.3046875 z: -27.4476470947
x: 27.4921875 y: -47.2312469482 z: -28.5679626

You can add additional bridge methods to your pyobjus iOS app by changing the content of the bridge.m/.h files, or by adding completely new files and classes to your xcode project. After that, you can consume them with pyobjus using the methods illustrated above.

Pyobjus-ball example

We’ve made a simple example using the accelerometer to control a ball on screen. In addition, with this example, you can set you screen brightness using a kivy slider.

We won’t go into the details of the kivy language or kivy itself as you can find excellent examples and docs on the official kivy site.

So, here is the code of the main.py file:

from random import random
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import NumericProperty, ReferenceListProperty, ObjectProperty
from kivy.vector import Vector
from kivy.clock import Clock
from kivy.graphics import Color
from pyobjus import autoclass

class Ball(Widget):

    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)
    h = NumericProperty(0)
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    def move(self):
        self.pos = Vector(*self.velocity) + self.pos

class PyobjusGame(Widget):

    ball = ObjectProperty(None)
    screen = ObjectProperty(autoclass('UIScreen').mainScreen())
    bridge = ObjectProperty(autoclass('bridge').alloc().init())
    sensitivity = ObjectProperty(50)
    br_slider = ObjectProperty(None)

    def __init__(self, *args, **kwargs):
        super(PyobjusGame, self).__init__()
        self.bridge.startAccelerometer()

    def __dealloc__(self, *args, **kwargs):
        self.bridge.stopAccelerometer()
        super(PyobjusGame, self).__dealloc__()

    def reset_ball_pos(self):
        self.ball.pos = self.width / 2, self.height / 2

    def on_bright_slider_change(self):
        self.screen.brightness = self.br_slider.value

    def update(self, dt):
        self.ball.move()
        self.ball.velocity_x = self.bridge.ac_x * self.sensitivity
        self.ball.velocity_y = self.bridge.ac_y * self.sensitivity

        if (self.ball.y < 0) or (self.ball.top >= self.height):
            self.reset_ball_pos()
            self.ball.h = random()

        if (self.ball.x < 0) or (self.ball.right >= self.width):
            self.reset_ball_pos()
            self.ball.h = random()


class PyobjusBallApp(App):

    def build(self):
        game = PyobjusGame()
        Clock.schedule_interval(game.update, 1.0/60.0)
        return game


if __name__ == '__main__':
    PyobjusBallApp().run()

And the contents of pyobjusball.kv are:

<Ball>:
    size: 50, 50
    h: 0
    canvas:
        Color:
            hsv: self.h, 1, 1,
        Ellipse:
            pos: self.pos
            size: self.size

<PyobjusGame>:
    ball: pyobjus_ball
    br_slider: bright_slider

    Label:
        text: 'Screen brightness'
        pos: bright_slider.x, bright_slider.y + bright_slider.height / 2
    Slider:
        pos: self.parent.width / 4, self.parent.height / 1.1
        id: bright_slider
        value: 0.5
        max: 1
        min: 0
        width: self.parent.width / 2
        height: self.parent.height / 10
        on_touch_up: root.on_bright_slider_change()

    Ball:
        id: pyobjus_ball
        center: self.parent.center

Now create a directory with the name pyobjus-ball and place the files above in it:

mkdir pyobjus-ball
mv main.py pyobjus-ball
mv pyobjusball.kv pyobjus-ball

In this step, we assume that you have already have downloaded and built kivy-ios. Navigate to the directory where kivy-ios is located, then execute the following commands:

tools/create-xcode-project.sh pyobjusBall /path/to/pyobjus-ball
open app-pyobjusball/pyobjusball.xcodeproj/

After this step, xcode will open and, if you have connected your iOS device to your computer, you can run the project and will see your app running on your device.

This is a screenshoot from an iPad.

_images/IMG_0330.PNG