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 Handler class extends NSObject. 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 in Your first bridge.
The v argument on initWithValue_() 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 type id - 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 return id. A return type annotation of None will be interpreted as a void method in Objective-C. The name argument doesn’t need to be annotated because it will be passed in as a string, and strings are NSObject subclasses in Objective-C.
initWithValue_() is a constructor, so it returns self.
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 wont 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: ObjC runtime already contains a registered class named 'Handler'.
You’ll need to be careful (and sometimes, painfully verbose) when choosing class names.
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 lifecycle - which means defining initializer methods that are visible to the Objective-C runtime, and invoking them over that bridge.
Next steps#
???