humancode.us

Dealing with Objective-C Protocol types in Swift

September 19, 2024

When Objective-C headers are imported into Swift using a bridging header, a single Objective-C protocol appears as two different entities in Swift code.

Say we have the following Objective-C header file that has been imported into a Swift target using a bridging header:

// Fungible.h

@protocol Fungible<NSObject>
- (NSString *)funge;
@end

There are two ways to refer to this protocol in Swift: using a native Swift type, or using an Objective-C Protocol object reference.

let fungibleSwiftProtocol = Fungible.self
let fungibleObjCProtocol = NSProtocolFromString("Fungible")!

These two types should be the same—after all, they refer to the same protocol—but they are not, as shown when we print out their descriptions and ObjectIdentifiers:

print("fungibleSwiftProtocol: \(fungibleSwiftProtocol)")
print("fungibleSwiftProtocol object identifier: \(ObjectIdentifier(fungibleSwiftProtocol))")
print("fungibleObjCProtocol: \(fungibleObjCProtocol)")
print("fungibleObjCProtcol object identifier: \(ObjectIdentifier(fungibleObjCProtocol))")

Which produces the output:

fungibleSwiftProtocol: Fungible
fungibleSwiftProtocol object identifier: ObjectIdentifier(0x00000001f5d8e300)
fungibleObjCProtocol: <Protocol: 0x100008780>
fungibleObjCProtcol object identifier: ObjectIdentifier(0x0000000100008780)

They are different! But of course they’re different, you say: one of them is a Swift Any.Type, while the other is a Protocol instance. Sure, but they refer to the same thing. Is there a way to actually test if they refer to the same thing?

Swift types that refer to Objective-C protocols can be converted to Protocol instances

The answer is yes: there is a way to convert a Swift type that refers to an Objective-C protocol into an Objective-C Protocol reference.

Here is that magic invocation:

if let fungibleSwiftProtocolAsObjCProtocol = fungibleSwiftProtocol as AnyObject as? Protocol {
    print("fungibleSwiftProtocol is an ObjC protocol: \(fungibleSwiftProtocolAsObjCProtocol)")
    print("with the object identifier: \(ObjectIdentifier(fungibleSwiftProtocolAsObjCProtocol))")
} else {
    print("fungibleSwiftProtocol is not an ObjC protocol.")
}

Which produces the output:

fungibleSwiftProtocol is an ObjC protocol: <Protocol: 0x100008780>
with the object identifier: ObjectIdentifier(0x0000000100008780)

Note that the object identifier printed here is equal to the object identifier of the Objective-C Protocol instance printed above. We have successfully discovered the Protocol instance that corresponds to a Swift type, and now comparisons can be made.

It’s interesting to me that Any.Type can always be converted into AnyObject. For pure Swift types, the object you get will be an instance of some internal class like __SwiftValue.

The reverse conversion cannot be done

As far as I can tell, there is no way to take an Objective-C Protocol instance and end up with a Swift Any.Type value that refers to the bridged type in Swift.

That means that any code that needs to deduplicate Swift protocol types that come from an Objective-C bridging header, and an actual Protocol instance of that protocol, must use the technique above to convert the Swift type into a Protocol instance. If the two instances are identical, then they are the same type.

Who the heck needs this?

If you write enough code that straddles the boundary between Swift and Objective-C, you’ll eventually run into problems like these. Understanding how Swift types and Objective-C protocols are related in Swift allows you to normalize and deduplicate these type references in your code.