You’re just not my type: Using Objective-C types in Python

Objective-C is a strong, static typed language. Every variable has a specific type, and that type cannot change over time. If a function declares that it accepts an integer, then it must receive a variable that is declared as an integer, or an expression that results in an integer.

Python, on the other hand, is strong, but dynamically typed language. Every variable has a specific type, but that type can be modified or interpreted in other ways. When a function accepts an argument, Python will allow you to pass any variable, of any type.

So, if you want to bridge between Objective-C and Python, you need to be able to provide static typing information so that Python can work out how to convert a variable of arbitrary type into a specific type matching Objective-C’s expectations.

If you’re calling an Objective C method defined in a library, this conversion is done automatically - the Objective-C runtime contains enough information for Rubicon to infer the required types. However, if you’re defining a new method (or a method override) in Python, we need to provide that typing information. To do this, we use Python 3’s type annotation. Here’s how.

Primitives

If a Python value needs to be passed in as a primitive, Rubicon will wrap the primitive:

Value C primitive
bool 8 bit integer (although it can only hold 2 values - 0 and 1)
int 32 bit integer
float double precision floating point

If a Python value needs to be passed in as an object, Rubicon will wrap the primitive in an object:

Value Objective C type
bool NSNumber (bool)
int NSNumber (long)
float NSNumber (double)

If you’re declaring a method and need to annotate the type of an argument, the Python type name can be used as the annotation type. You can also use any of the ctypes primitive types. Rubicon also provides type definitions for common Objective-C typedefs, like NSInteger, CGFloat, and so on.

Strings

If a method calls for an NSString argument, you can provide a Python str for that argument. Rubicon will construct an NSString instance from the data in the str provided, and pass that value for the argument.

If a method returns an NSString, the return value will be a wrapped ObjCStrInstance type. This type implements a str-like interface, wrapped around the underlying NSString data. This means you can treat the return value as if it were a string - slicing it, concatenating it with other strings, comparing it, and so on:

# Call an Objective C method that returns a string.
# We're using NSBundle to give us a string version of a path
>>> NSBundle.mainBundle.bundlePath
<rubicon.objc.collections.ObjCStrInstance 0x114a94d68: __NSCFString at 0x7fec8ba7fbd0: /Users/brutus/path/to/somewhere>

# Slice the Objective C string
>>> NSBundle.mainBundle.bundlePath[:14]
<rubicon.objc.collections.ObjCStrInstance 0x114aa80f0: __NSCFString at 0x7fec8ba7fbd0: /Users/brutus/>

Note that ObjCStrInstance objects behave slightly differently than Python str objects in some cases. For technical reasons, ObjCStrInstance objects are not hashable, which means they cannot be used as dict keys (but they can be used as NSDictionary keys). ObjCStrInstance also handles Unicode code points above U+FFFF differently than Python str, because the underlying NSString is based on UTF-16.

If you have an ObjCStrInstance instance, and you need to pass that instance to a method that does a specific typecheck for str, you can use str(nsstring) to convert the ObjCStrInstance instance to str:

# Convert the Objective C string to a Python string.
>>> str(NSBundle.mainBundle.bundlePath)
'/Users/rkm/projects/beeware/venv3.6/bin'

Conversely, if you have a str, and you specifically require a ObjCStrInstance instance, you can use the at() method to convert the Python instance to an ObjCStrInstance.

>>> from rubicon.objc import at
# Create a Python string
>>> py_str = 'hello world'
# Convert to an Objective C string
>>> at(py_str)
<rubicon.objc.collections.ObjCStrInstance 0x114a94e48: __NSCFString at 0x7fec8ba7fc10: hello world>

ObjCStrInstance implements all the utility methods that are available on str, such as replace and split. When these methods return a string, the implementation may return Python str or ObjCStrInstance instances. If you need to use the return value from these methods, you should always use str() to ensure you have a Python string:

# Is the path comprised of all lowercase letters? (Hint: it isn't)
>>> NSBundle.mainBundle.bundlePath.islower()
False

# Convert string to lower case; use str() to ensure we get a Python string.
>>> str(NSBundle.mainBundle.bundlePath.lower())
'/users/rkm/projects/beeware/venv3.6/bin'

Lists

If a method calls for an NSArray or NSMutableArray argument, you can provide a Python list for that argument. Rubicon will construct an NSMutableArray instance from the data in the list provided, and pass that value for the argument.

If a method returns an NSArray or NSMutableArray, the return value will be a wrapped ObjCListInstance type. This type implements a list-like interface, wrapped around the underlying NSArray data. This means you can treat the return value as if it were a list - iterating over values, retrieving objects by index, and so on.

Dictionaries

If a method calls for an NSDictionary or NSMutableDictionary argument, you can provide a Python dict. Rubicon will construct an NSMutableDictionary instance from the data in the dict provided, and pass that value for the argument.

If a method returns an NSDictionary or NSMutableDictionary, the return value will be a wrapped ObjCDictInstance type. This type implements a dict-like interface, wrapped around the underlying NSDictionary data. This means you can treat the return value as if it were a dict - iterating over keys, values or items, retrieving objects by key, and so on.

NSPoint, NSSize, and NSRect

On instances of an Objective C structure, each field is exposed as a Python attribute. For example, if you create an instance of an NSSize object you can access its width and height by calling NSSize.width().

When you need to pass an Objective C structure to an Objective C method, you can pass a tuple instead. For example, if you pass (10.0, 5.1) where a NSSize is expected, it will be converted automatically in the appropriate width, height for the structure.