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):
    def initWithValue_(self, v: int):
        self.value = v
        return self

    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)
>>> print(my_handler.pokeWithValue(37, andName="Alice"))
My name is Alice

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

    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