pyobjus/docs/source/pyobjus_ios.rst

460 lines
16 KiB
ReStructuredText

.. _pyobjus_ios:
Pyobjus on iOS
==============
You may be wondering how to run pyobjus on iOS devices?
The solution to this problem is to use
`kivy-ios <https://github.com/kivy/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 <http://kivy.org/docs/tutorials/firstwidget.html>`_
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 screenshot from my iPad.
.. figure:: images/IMG_0322.PNG
:align: center
:scale: 30%
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 screenshot from an iPad.
.. figure:: images/IMG_0330.PNG
:align: center
:scale: 30%