Tutorial 2 - Writing your own class¶
Eventually, you'll come across an Objective-C API that requires you to provide a class instance as an argument. For example, when using macOS and iOS GUI classes, you often need to define "delegate" classes to describe how a GUI element will respond to mouse clicks and key presses.
Let's define a Handler class, with two methods:
- an
-initWithValue:constructor that accepts an integer; and - a
-pokeWithValue:andName:method that accepts an integer and a string, prints the string, and returns a float that is one half of the value.
The declaration for this class would be:
from rubicon.objc import NSObject, objc_method
class Handler(NSObject):
@objc_method
def initWithValue_(self, v: int):
self.value = v
return self
@objc_method
def pokeWithValue_andName_(self, v: int, name) -> float:
print("My name is", name)
return v / 2.0
This code has several interesting implementation details:
- The
Handlerclass extendsNSObject. This instructs Rubicon to construct the class in a way that it can be registered with the Objective-C runtime. - Each method that we want to expose to Objective-C is decorated with
@objc_method.The method names match the Objective-C descriptor that you want to expose, but with colons replaced by underscores. This matches the "long form" way of invoking methods discussed intutorial-1. - The
vargument oninitWithValue_()uses a Python 3 type annotation to declare it's type. Objective-C is a language with static typing, so any methods defined in Python must provide this typing information. Any argument that isn't annotated is assumed to be of typeid- that is, a pointer to an Objective-C object. - The
pokeWithValue_andName_()method has it's integer argument annotated, and has it's return type annotated as float. Again, this is to support Objective-C typing operations. Any function that has no return type annotation is assumed to returnid. A return type annotation ofNonewill be interpreted as avoidmethod in Objective-C. Thenameargument doesn't need to be annotated because it will be passed in as a string, and strings areNSObjectsubclasses in Objective-C. initWithValue_()is a constructor, so it returnsself.
Having declared the class, you can then instantiate and use it:
>>> my_handler = Handler.alloc().initWithValue(42)
>>> print(my_handler.value)
42
>>> print(my_handler.pokeWithValue(37, andName="Alice"))
My name is Alice
18.5
Objective-C properties¶
When we defined the initializer for Handler, we stored the provided value as the value attribute of the class. However, as this attribute wasn't declared to Objective-C, it won't be visible to the Objective-C runtime. You can access value from within Python - but Objective-C code won't be able to access it.
To expose value to the Objective-C runtime, we need to make one small change, and explicitly declare value as an Objective-C property:
from rubicon.objc import NSObject, objc_method, objc_property
class PureHandler(NSObject):
value = objc_property()
@objc_method
def initWithValue_(self, v: int):
self.value = v
return self
This doesn't change anything about how you access or modify the attribute - it just means that Objective-C code will be able to see the attribute as well.
Class naming¶
In this revised example, you'll note that we also used a different class name - PureHandler. This was deliberate, because Objective-C doesn't have any concept of namespaces. As a result, you can only define one class of any given name in a process - so, you won't be able to define a second Handler class in the same Python shell. If you try, you'll get an error:
>>> class Handler(NSObject):
... pass
Traceback (most recent call last)
...
RuntimeError: An Objective-C class named b'Handler' already exists
You'll need to be careful (and sometimes, painfully verbose) when choosing class names.
To allow a class name to be reused, you can set the class variable ~rubicon.objc.api.ObjCClass.auto_rename to True. This option enables automatic renaming of the Objective-C class if a naming collision is detected:
>>> ObjCClass.auto_rename = True
This option can also be enabled on a per-class basis by using the auto_rename argument in the class declaration:
>>> class Handler(NSObject, auto_rename=True):
... pass
If this option is used, the Objective-C class name will have a numeric suffix (e.g., Handler_2). The Python class name will be unchanged.
What, no __init__()?¶
You'll also notice that our example code doesn't have an __init__() method like you'd normally expect of Python code. As we're defining an Objective-C class, we need to follow the Objective-C object life cycle - which means defining initializer methods that are visible to the Objective-C runtime, and invoking them over that bridge.