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