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; anda
-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 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 in Your first bridge.The
v
argument 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 ofNone
will be interpreted as avoid
method in Objective-C. Thename
argument doesn’t need to be annotated because it will be passed in as a string, and strings areNSObject
subclasses 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 re-used, you can set the class variable
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.
Next steps¶
???