My career is kind of colourful. I was working on aircraft systems, VoIP HW & SW, developing Linux kernel drivers, Linux distributions, OpenOffice.org and many other low level (RTOS) & high level (desktop applications) stuff. Linux nerd. I said enough one day. I wanted to just work and wasn’t willing to continue wasting my time with searching how to workaround this and that. Bye bye Linux, did spend nice 10 years with you. Not saying it was a bad experience. Nope, I learned a lot and enjoyed it. But …
Fast forward, I’m with Apple for more than 10 years now. Hmm, another 10 years, time to leave? Nope! Honestly, not excited as I was when I switched, but still pretty happy. Remember the first iPhone? iPad? Magical devices. Not mentioning first SDK. Exciting times. I did develop lot of applications for iPhones, iPads and for the desktop. Learned a lot about Objective-C, Objective-C runtime and all these Apple platforms.
I also bought every iPad Apple did release. But I didn’t know what to do with it. Kind of device for content consumers. I tried, really hard, to use it as a device where I can create something. But I always struggled, I always gave up and go back to my Mac. Apparently I was trying to use it for something it wasn’t designed for. Or it was, but iOS itself wasn’t ready for it.
My needs changed few months ago. I no longer work as a full time developer, but rather trying to lead people and working on new product specifications. Hard change. Lot of new things to learn.
Do I need to travel with my 15" MacBook Pro for this kind of work? Am I willing to try iPad again? Yup, I am. Bought the iPad Pro 12.9", external keyboard and pencil few months ago. iOS 11, Files, drag & drop, wonderful applications like Workflow, OmniGraffle, Editorial, Ulysses, MindNode , etc. I have to say that the iPad Pro makes sense as a device where I can create stuff finally. Took quite some time, but it seems it’s already here. Suits my needs perfectly.
What all this has to do with Python IDE on the iPad Pro? I’m no longer working on our products, but I didn’t left programming. And I never will. It’s quite refreshing to hide myself with all these devices in some place where no one can disturb me from time to time. We do use Amazon Web Services a lot. I’m still kinda involved in scripting, support tools, … and we do write all these things in Python. Amazon provides nice package named boto3:
Boto is the Amazon Web Services (AWS) SDK for Python, which allows Python developers to write software that makes use of Amazon services like S3 and EC2. Boto provides an easy to use, object-oriented API as well as low-level direct service access.
I wrote lot of tools, scripts, … leveraging boto3 power to maintain our AWS services. Also wrote some django applications for internal use. Can I do this on my iPad Pro? Quite some effort, but yes, I can! And now, you’re finally going to learn how.
Pythonista for iOS
Hats off to Ole for writing Pythonista for iOS. Seriously, tremendous amount of work. Quote from the Pythonista website:
Pythonista is a complete development environment for writing Python™ scripts on your iPad or iPhone. Lots of examples are included — from games and animations to plotting, image manipulation, custom user interfaces, and automation scripts.
In addition to the powerful standard library, Pythonista provides extensive support for interacting with native iOS features, like contacts, reminders, photos, location data, and more.
Pythonista supports Python 2.7, 3.5 (beta includes 3.6), standard library, lot of bundled packages and neat support for native iOS features.
Release cycle of Pythonista is quite long. Version 3.0 was released an year ago, version 3.1 just a few days ago. This release cycle leads to a situation, where bundled packages can be outdated and native iOS features doesn’t support latest & greatest stuff — drag & drop, Files.app, etc.
What can be done about this? There’s project named StaSh — Shell for Pythonista. Do I need shell you say? Bet you do. StaSh does contain useful commands like pip, which allows you to install additional packages from PyPI. Not all packages are supported. Package must be pure Python, because of the way how binaries, signatures, … work on iOS. It’s more complicated, but I’m not going to explain it here. Just try it and you’ll see if your package will be installed or not.
Remember the first iPhone OS SDK? First apps? Did you ever think this will be possible? And we just started.
You should really buy Pythonista if you’re into Python.
Dark side of the moon
You’re excited, you started to love Pythonista, but you slowly do realize that you miss something. Keyboard shortcuts, features like Jump to definition, Open quickly and lot of other things. Stuff which makes great IDE great.
I started filling issues as a first thing. There’s more than 330 open issues. Hmm, how quickly will Ole solve them? Am I willing to wait weeks, months, …? Isn’t my use case minor one? Will they ever be fixed? Lot of questions. I’m a problem solver guy, so, I decided to fix it by myself. That’s how the Black Mamba project started.
Prerequisites
I assume that you’re familiar with following topics:
- Objective-C Runtime  — messaging, signatures and type encoding,
- Event handling, responders and the responder chain — UIResponder, UIKeyCommand and UIEvent.
Also I highly recommend to look at ctypes and objc_util in advance.
Keyboard shortcuts
Pythonista provides small amount of shortcuts and I can’t work without them. Still consider screen tapping as a slow thing. Let’s add some of them. We have two basic requirements:
- be at the end of the responder chain, just to avoid clashes with Pythonista view controllers shortcuts,
- have them working even if the view controller hierarchy changes over time.
It seems that the keyCommands
property of the UIApplication
perfectly suits our needs. When
compared to the UIViewController
, there’s no addKeyCommand:
and we have to found another way how
to modify keyCommands
result. Time to poke Objective-C runtime, especially
class_addMethod
and
method_exchangeImplementations
functions. First one allows us to add new methods and second one allows us to exchange their
implementations. It’s called swizzling. We will need to use these functions more than once, so,
here are Python wrappers:
- add_method — adds new method to the class,
- swizzle - adds new method to the class and exchange implementations with the original one.
Ready for swizzling? Let’s write custom keyCommands
function in Python:
def _blackmamba_keyCommands(_self, _cmd):
obj = ObjCInstance(_self)
commands = list(obj.originalkeyCommands() or [])
commands.extend(_key_commands)
return ns(commands).ptr
Simple one, which gets the original list of key commands and adds our ones from the _key_commands
list (module global variable). We do not need to add new method explicitly, our swizzle
function
does it automatically:
swizzle('UIApplication', 'keyCommands', _blackmamba_keyCommands)
What happened? A picture is worth a thousand words:
The only remaining thing now is to introduce function, which allows us to register keyboard shortcuts — register_key_command. Signature of this function follows.
def _register_key_command(input, modifier_flags, function, title=None)
input
- it can be any string (like lettera
for example) or UIKeyInput enum, which does contain values for special keys like left arrow.modifier_flags
- it can beint
(if you know the correct value) or you can use UIKeyModifier enum.function
- Python function, no arguments, to call when user presses the shortcut.title
- optionalstr
, discoverability title.
How to print Hallo with Cmd H
?
from blackmamba.uikit.keyboard import (
register_key_command, UIKeyModifier
)
def hallo():
print('Hallo')
register_key_command(
'h',
UIKeyModifier.command,
hallo
)
Passed function hallo
is wrapped with a function, which does meet Objective-C runtime specs.
def key_command_action(_sel, _cmd, sender):
function()
Based on input
and modifier_flags
, method name is generated. blackMambaHandleKeyCommandH:
in this case.
blackMambaHandleKeyCommandH:
method is added to the UIApplication
with implementation pointing
to the key_command_action
. UIKeyCommand
object is created and stored in the _key_commands
global list.
And that’s it. We have a way how to register custom keyboard shortcut, assign it to a function written in Python.
Pretty awesome and I can’t still believe it’s possible.
Keyboard events
Imagine you have a dialog and you would like to use keyboard shortcuts for it as well. Like arrow
keys to change selection, enter to confirm selection, Cmd .
to close dialog, etc. Global shortcuts
can be reused for this task, but it will bring another sort of issues. We will be forced to remember
which dialog is active, pass events to it from our shortcut handler, forget the dialog when
it’s closed, … Sounds complicated to me.
Let’s solve this task in a similar way, by swizzling handleKeyUIEvent:
(UIApplication
). This
method receives physical keyboard events (type = 4
) and we can handle them there.
This is private API, don’t do this in your applications.
Because it’s very similar to global keyboard shortcuts, here’re just links to specific implementations:
- _blackmamba_handleKeyUIEvent
 — our implementation of
handleKeyUIEvent:
, - register_key_event_handler — event registration,
- unregister_key_event_handler — event unregistration.
And an example how to register and unregister keyboard events.
Drag &Â drop
There was no drag & drop support prior to Pythonista (311013, beta). I desperately did want to use it and wrote another script — drag_and_drop.py.
This script demonstrates how to create custom classes, classes that implements protocols like
UITableViewDragDelegate
and how to use blocks. I think it’s pretty self explanatory except one thing.
There’s experimental support of blocks in Pythonista. You can write a Python function and create block from it. Here’s an example:
def block_imp(_cmd):
print('Hallo')
block = ObjCBlock(
block_imp,
restype=ctypes.c_void_p,
argtypes=[ctypes.c_void_p, ctypes.c_void_p]
)
If you need to pass block as an argument somewhere, you can just pass block
variable now and that’s it.
Works. But what if you have to implement block, which receives another block as an argument. There’s
no support for it in Pythonista.
I wrote the
ObjCBlockPointer
class, which can be initialised with the block pointer. And then you can call it. Here’s
an example
how to use this class. Drag session delegate, which is supposed to load data and then call another block
when the operation completes. It’s achieved via ObjCBlockPointer
class.
Conclusion
Do you need additional packages like me? Just install
StaSh and use pip
. I did install django
, boto3
and many other
packages. Do you miss some functionality in Pythonista? Install
Black Mamba.
I like Pythonista for iOS a lot. I do use it almost on a daily basis even when it’s not perfect (nothing’s perfect) and lacks some features. But because it’s Python IDE, we have an access to the Objective-C runtime, nothing can stop you from enhancing it on your own. Like I did. See the gallery of script I wrote.
Can the iPad Pro replace my desktop computer? Yup, it can. There’re still some edge cases where I rather use my MacBook Pro, but the number of them is very low.
Did you ever think that it will be possible to do these kind of things? In an App Store application, no jailbreak, … Me not, never, until now. And I can’t still kind of believe it.
Learn or die and remember, sky is the limit 🙂
Again, many thanks to Ole for writing this wonderful piece of software! Please, buy it to support Ole’s ongoing effort to make it better.
Links
Django notes
If you’d like to run Django application on your iPad Pro, don’t forget to pass following arguments:
dir = os.path.abspath(os.path.join(os.path.dirname(path), '../application'))
sys.path.append(dir)
# import Django execute_from_command_line
arguments = list(sys.argv)
arguments.append('runserver')
arguments.append('--noreload')
arguments.append('--nothreading')
arguments.append('-v')
arguments.append('0')
logging.disable(logging.CRITICAL)
try:
execute_from_command_line(arguments)
except KeyboardInterrupt:
logging.disable(logging.NOTSET)
sys.path.remove(dir)
Why these arguments? Django is very talkative and it is constantly filling your console with messages.
Every single time there’s a new message, Pythonista will reveal console, tab with your app becomes
inactive and you have to tap on it to reveal your application. Kind of … That’s the reason for
-v 0
and disabled logging.
No threading & reloading is here, because Pythonista supports only one Python thread.
Still some limitations, but better than nothing. Otherwise it works perfectly.