Using and creating Objective-C protocols

Protocols are used in Objective-C to declare a set of methods and properties for a class to implement. They have a similar purpose to ABCs (abstract base classes) in Python.

Looking up a protocol

Protocol objects can be looked up using the ObjCProtocol constructor, similar to how classes can be looked up using ObjCClass:

>>> NSCopying = ObjCProtocol('NSCopying')
>>> NSCopying
<ObjCProtocol: NSCopying>

The isinstance function can be used to check whether an object conforms to a protocol:

>>> isinstance(NSObject.new(), NSCopying)
False
>>> isinstance(NSArray.array(), NSCopying)
True

Implementing a protocol

When writing a custom Objective-C class, you might want to have it conform to one or multiple protocols. In Rubicon, this is done by using the protocols keyword argument in the base class list. For example, if you have a class UserAccount and want it to conform to NSCopyable, you would write it like this:

class UserAccount(NSObject, protocols=[NSCopying]):
    username = objc_property()
    emailAddress = objc_property()

    @objc_method
    def initWithUsername_emailAddress_(self, username, emailAddress):
        self = self.init()
        if self is None:
            return None
        self.username = username
        self.emailAddress = emailAddress
        return self

    # This method is required by NSCopying.
    # The "zone" parameter is obsolete and can be ignored, but must be included for backwards compatibility.
    # This method is not normally used directly. Usually you call the copy method instead,
    # which calls copyWithZone: internally.
    @objc_method
    def copyWithZone_(self, zone):
        return UserAccount.alloc().initWithUsername(self.username, emailAddress=self.emailAddress)

We can now use our class. The copy method (which uses our implemented copyWithZone: method) can also be used:

>>> ua = UserAccount.alloc().initWithUsername_emailAddress_(at('person'), at('person@example.com'))
>>> ua
<ObjCInstance: UserAccount at 0x106543210: <UserAccount: 0x106543220>>
>>> ua.copy()
<ObjCInstance: UserAccount at 0x106543210: <UserAccount: 0x106543220>>

And we can check that the class conforms to the protocol:

>>> isinstance(ua, NSCopying)
True

Writing custom protocols

You can also create custom protocols. This works similarly to creating custom Objective-C classes:

class Named(metaclass=ObjCProtocol):
    name = objc_property()

    @objc_method
    def sayName(self):
        ...

There are two notable differences between creating classes and protocols:

  1. Protocols do not need to extend exactly one other protocol - they can also extend multiple protocols, or none at all. When not extending other protocols, as is the case here, we need to explicitly add metaclass=ObjCProtocol to the base class list, to tell Python that this is a protocol and not a regular Python class. When extending other protocols, Python detects this automatically.

  2. Protocol methods do not have a body. Python has no dedicated syntax for functions without a body, so we put ... in the body instead. (You could technically put code in the body, but this would be misleading and is not recommended.)