Search code examples
iosobjective-cswiftobjc-bridging-header

Accessing property of forwardly declared enum from swift


Given that there is an ObjC compatible enum written in Swift:

// from MessageType.swift
@objc enum MessageType: Int {
    case one
    case two
}

and an ObjC class with a property of type MessageType which has to be forwardly declared:

// from Message.h
typedef NS_ENUM(NSInteger, MessageType);

@interface Message: NSObject
@property (nonatomic, readonly) MessageType messageType;
@end

In order to use the Messagein the rest of the Swift codebase, the Message.h was added into the bridging header:

// from App-Bridging-Header.h
#import "Message.h"

Now, imagine there is a Swift class that tries to read the messageType property:

// from MessageTypeReader.swift
class MessageTypeReader {
    static func readMessageType(of message: Message) -> MessageType {
        return message.messageType
    }
}

The compilation would fail with the following error:

Value of type 'Message' has no member 'messageType'

My question would be: Is there a way to forwardly declare a Swift enum in order for the MessageTypeReader to be able to access the property?

Note: I am aware of the possibility of rewriting the Message into Swift or importing App-Bridging-Header.h into Message.h, but that is not an option here, I am looking for a solution that would work with the current setup.


Solution

  • I guess one reason to use NS_ENUM on Objective-C side is to have compile time checks whether the switch statement usages are exhaustive.

    If that's the case one could utilize C unions.

    Objective-C Header

    typedef NS_ENUM(NSInteger, MessageType);
    
    union MessageTypeU {
        MessageType objc;
        NSInteger swift;
    };
    
    
    @interface Message : NSObject
    
    @property (nonatomic, readonly) union MessageTypeU messageType;
    
    @end
    

    So the basic idea is:

    Swift imports C unions as Swift structures. Although Swift doesn’t support natively declared unions, a C union imported as a Swift structure still behaves like a C union.

    ...

    Because unions in C use the same base memory address for all of their fields, all of the computed properties in a union imported by Swift use the same underlying memory. As a result, changing the value of a property on an instance of the imported structure changes the value of all other properties defined by that structure.

    see here: https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/using_imported_c_structs_and_unions_in_swift

    Objective-C Implementation Example

    @interface Message ()
    
    @property (nonatomic, readwrite) union MessageTypeU messageType;
    
    @end
    
    
    @implementation Message
    
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _messageType.objc = MessageTypeTwo;
            [self testExhaustiveCompilerCheck];
        }
        return self;
    }
    
    - (void)testExhaustiveCompilerCheck {
        
        switch(self.messageType.objc) {
            case MessageTypeOne:
                NSLog(@"messageType.objc: one");
                break;
            case MessageTypeTwo:
                NSLog(@"messageType.objc: two");
                break;
        }
        
    }
    
    @end
    

    Usage on Swift Side

    Since the messageType.swift property comes originally from the Swift side (see definition of MessageType) we can safely use force-unwrap.

    class MessageTypeReader {
    
        static func readMessageType(of message: Message) -> MessageType {
            return MessageType(rawValue: message.messageType.swift)!
        }
        
    }