Accessing property of forwardly declared enum from swift
Asked Answered
E

2

17

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.

Ebeneser answered 6/12, 2018 at 10:39 Comment(0)
R
12

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)!
    }
    
}
Reimburse answered 9/12, 2018 at 12:49 Comment(1)
Thanks for this workaround, my only concern is that it changes the API as there will have to be extra .swift and .objc for each enum, perhaps I should have been even more specific in the question.Ebeneser
P
6

Here is a workaround suggested by Cristik (all credit goes to them):

  • In Message.h, declare messageType as NSInteger :

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

    Using NS_REFINED_FOR_SWIFT is recommended by Apple, but not necessary here.

  • In Swift, add the following Message extension :

    extension Message {
        var messageType: MessageType {
            guard let type = MessageType(rawValue: self.__messageType) else {
                fatalError("Wrong type")
            }
            return type
        }
    }
    
Pettitoes answered 9/12, 2018 at 10:31 Comment(2)
Isn't this going to make messageType not enumerable in ObjC?Ebeneser
@MiroslavKovac You're correct. Currently, circular references aren't supported.Pettitoes

© 2022 - 2024 — McMap. All rights reserved.