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

Objective-C is a strongly and statically-typed language. Every variable has a specific type, and that type cannot change over time. Function parameters also have fixed types, and a function will only accept arguments of the correct types.

Python, on the other hand, is a strongly, but dynamically-typed language. Every object has a specific type, but all variables can hold objects of any type. When a function accepts an argument, Python will allow you to pass any object, 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 Rubicon can work out how to convert a Python object of arbitrary type into a specific type matching Objective-C’s expectations.

Type annotations#

If you’re calling an Objective-C method defined in a library, its types are already known to the Objective-C runtime and Rubicon. However, if you’re defining a new method (or a method override) in Python, you need to manually provide its types. This is done using Python 3’s type annotation syntax.

Passing and returning Objective-C objects doesn’t require any extra work — if you don’t annotate a parameter or the return type, Rubicon assumes that it is an Objective-C object. (To define a method that doesn’t return anything, you need to add an explicit -> None annotation.)

All other parameter and return types (primitives, pointers, structs) need to be annotated to tell Rubicon and Objective-C which type to expect. These annotations can use any of the types defined by Rubicon, such as NSInteger or NSRange, as well as standard C types from the ctypes module, such as c_byte or c_double.

For example, a method that takes a C double and returns a NSInteger would be defined and annotated like this:

@objc_method
def roundToZero_(self, value: c_double) -> NSInteger:
    return int(value)

Rubicon also allows certain Python types to be used in method signatures, and converts them to matching primitive ctypes types. For example, Python int is treated as c_int, and float is treated as c_double.

See also

The rubicon.objc.types reference documentation lists all C type definitions provided by Rubicon, and provides additional information about how Rubicon converts types.

Type conversions#

When you call existing Objective-C methods, Rubicon already knows which type each argument needs to have and what it returns. Based on this type information, Rubicon will automatically convert the passed arguments to the proper Objective-C types, and the return value to an appropriate Python type. This makes explicit type conversions between Python and Objective-C types unnecessary in many cases.

Argument conversion#

If an Objective-C method expects a C primitive argument, you can pass an equivalent Python value instead. For example, a Python int value can be passed into any integer argument (int, NSInteger, uint8_t, …), and a Python float value can be passed into any floating-point argument (double, CGFloat, …).

To pass a C structure as an argument, you would normally need to construct a structure instance by name. This can get somewhat lengthy, especially with nested structures (e. g. NSRect(NSPoint(1.2, 3.4), NSSize(5.6, 7.8))). As a shorthand, Rubicon allows passing tuples instead of structure objects (e. g. ((1.2, 3.4), (5.6, 7.8))) and automatically converts them to the required structure type.

If a parameter expects an Objective-C object, you can also pass certain Python objects, which are automatically converted to their Objective-C counterparts. For example, a Python str is converted to an NSString, bytes to NSData, etc. Collections are also supported: list and dict are converted to NSArray and NSDictionary, and their elements are converted recursively.

Note

All of these conversions can also be performed manually - see Manual conversions for details.

Return value conversion and wrapping#

Primitive values returned from methods are converted using the usual ctypes conversions, e. g. C integers are converted to Python int and floating-point values to Python float.

Objective-C objects are automatically returned as ObjCInstance objects, so you can call methods on them and access their properties. In some cases, Rubicon also provides additional Python methods on Objective-C objects - see Python-style APIs and methods for Objective-C objects for details.

Python-style APIs and methods for Objective-C objects#

For some standard Foundation classes, such as lists and dictionaries, Rubicon provides additional Python methods to make them behave more like their Python counterparts. This allows using Foundation objects in place of regular Python objects, so that you do not need to convert them manually.

Strings#

NSString objects behave almost exactly like Python str objects - they can be sliced, concatenated, compared, etc. with other Objective-C and Python strings.

# 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

ObjCInstance objects wrapping a NSString internally have the class ObjCStrInstance, and you will see this name in the repr() of NSString objects. This is an implementation detail - you should not refer to the ObjCStrInstance class explicitly in your code.

If you have an NSString, and you need to pass it to a method that does a specific type check for str, you can use str(nsstring) to convert the NSString 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 NSString, you can use the at() function to convert the Python instance to an NSString.

>>> 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>

NSString also supports 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 Objective-C NSString instances. If you need to use the return value from these methods, you should always use str or at() to ensure that you have the right kind of string for your needs.

# 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'

Note

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

Lists#

NSArray objects behave like any other Python sequence - they can be indexed, sliced, etc. and standard operations like len() and in are supported:

>>> from rubicon.objc import NSArray
>>> array = NSArray.arrayWithArray(list(range(4)))
>>> array[0]
0
>>> array[1:3]
<rubicon.objc.collections.ObjCListInstance 0x10b855208: __NSArrayI at 0x7f86f8e61950: <__NSArrayI 0x7f86f8e61950>(
1,
2
)
>
>>> len(array)
4
>>> 2 in array
True
>>> 5 in array
False

Note

ObjCInstance objects wrapping a NSArray internally have the class ObjCListInstance or ObjCMutableListInstance, and you will see these names in the repr() of NSArray objects. This is an implementation detail - you should not refer to the ObjCListInstance and ObjCMutableListInstance classes explicitly in your code.

NSMutableArray objects additionally support mutating operations, like item and slice assignment:

>>> from rubicon.objc import NSMutableArray
>>> mutarray = NSMutableArray.arrayWithArray(list(range(4)))
>>> mutarray[0] = 42
>>> mutarray
<rubicon.objc.collections.ObjCMutableListInstance 0x10b8558d0: __NSArrayM at 0x7f86fb04d9f0: <__NSArrayM 0x7f86fb04d9f0>(
42,
1,
2,
3
)
>
>>> mutarray[1:3] = [9, 8, 7]
>>> mutarray
<rubicon.objc.collections.ObjCMutableListInstance 0x10b8558d0: __NSArrayM at 0x7f86fb04d9f0: <__NSArrayM 0x7f86fb04d9f0>(
42,
9,
8,
7,
3
)
>

Sequence methods like index and pop are also supported:

>>> mutarray.index(7)
3
>>> mutarray.pop(3)
7

Note

Python objects stored in an NSArray are converted to Objective-C objects using the rules described in Argument conversion.

Dictionaries#

NSDictionary objects behave like any other Python mapping - their items can be accessed and standard operations like len() and in are supported:

>>> from rubicon.objc import NSDictionary
>>> d = objc.NSDictionary.dictionaryWithDictionary({"one": 1, "two": 2})
>>> d["one"]
1
>>> len(d)
>>> 2
>>> "two" in d
True
>>> "five" in d
False

Note

ObjCInstance objects wrapping a NSDictionary internally have the class ObjCDictInstance or ObjCMutableDictInstance, and you will see these names in the repr() of NSDictionary objects. This is an implementation detail - you should not refer to the ObjCDictInstance and ObjCMutableDictInstance classes explicitly in your code.

NSMutableDictionary objects additionally support mutating operations, like item assignment:

>>> md = objc.NSMutableDictionary.dictionaryWithDictionary({"one": 1, "two": 2})
>>> md["three"] = 3
>>> md
<rubicon.objc.collections.ObjCMutableDictInstance 0x10b8a7860: __NSDictionaryM at 0x7f86fb164b60: {
    one = 1;
    three = 3;
    two = 2;
}>

Mapping methods like keys and values are also supported:

>>> d.keys()
<rubicon.objc.collections.ObjCListInstance 0x10b898a90: __NSArrayI at 0x7f86f8db6b70: <__NSArrayI 0x7f86f8db6b70>(
one,
two
)
>
>>> d.values()
<rubicon.objc.collections.ObjCListInstance 0x10b8a7b38: __NSArrayI at 0x7f86f8c00370: <__NSArrayI 0x7f86f8c00370>(
1,
2
)
>

Note

Python objects stored in an NSDictionary are converted to Objective-C objects using the rules described in Argument conversion.

Manual conversions#

If necessary, you can also manually call Rubicon’s type conversion functions, to convert objects between Python and Objective-C when Rubicon doesn’t do so automatically.

Converting from Python to Objective-C#

The function ns_from_py() (also available as at() for short) can convert most standard Python objects to Foundation equivalents. For a full list of possible conversions, see the reference documentation for ns_from_py().

These conversions are performed automatically when a Python object is passed into an Objective-C method parameter that expects an object - in that case you do not need to call ns_from_py() manually (see Argument conversion).

Converting from Objective-C to Python#

The function py_from_ns() can convert many common Foundation objects to Python equivalents. For a full list of possible conversions, see the reference documentation for py_from_ns().

These conversions are not performed automatically by Rubicon. For example, if an Objective-C method returns an NSString, Rubicon will return it as an ObjCInstance (with some additional Python methods - see Python-style APIs and methods for Objective-C objects). Using py_from_ns(), you can convert the NSString to a real Python str.

When converting collections, such as NSArray or NSDictionary, py_from_ns() will convert them recursively to a pure Python object. For example, if nsarray is an NSArray containing NSStrings, py_from_ns(nsarray) will return a list of strs. In most cases, that is the desired behavior, but you can also avoid this recursive conversion by passing the Foundation collection into a Python collection constructor: for example list(nsarray) will return a list of NSStrings.