Accessing Swift OptionSetType in Objective-C

6.5k views Asked by At

In my Swift class, I have an OptionSetType defined for fulfillment options.

struct FulfillmentOption : OptionSetType {
    let rawValue: Int
    
    static let Pickup = FulfillmentOption(rawValue: 1 << 0)
    static let Shipping = FulfillmentOption(rawValue: 1 << 1)
    static let UserShipping = FulfillmentOption(rawValue: 1 << 2)
}

I then create a variable to add/remove and read options. This works as expected.

 var options: FulfillmentOption = []
 options.insert(FulfillmentOption.Pickup)
 options.contains(FulfillmentOption.Pickup)

However I need to access the options variable from one of my Objective-C classes. Since OptionSetType is not defined in Objective-C, the variable is not visible to any of my Objective-C classes.

What is the best way for me to expose this to Objective-C? Should I stop using OptionSetType altogether?

I've considered doing creating public and private variables like this to convert between the two. I don't love this, but it's the best I've come up with thus far.

private var _options: FulfillmentOptions = []
private var options: UInt {
  get {
    // get raw value from _options
  }
  set {
    // set raw value to _options
  }
}

Is there a more elegant way to accomplish this? I'd like to avoid writing unnecessary code.

2

There are 2 answers

2
Martin R On BEST ANSWER

Not a direct answer to your question, but as an alternative you can work the other way around. Define

typedef NS_OPTIONS(NSInteger, FulfillmentOption) {
    FulfillmentOptionPickup = 1 << 0,
    FulfillmentOptionShipping = 1 << 1,
    FulfillmentOptionUserShipping = 1 << 2,
};

in an Objective-C header, this would be imported into Swift as

public struct FulfillmentOption : OptionSetType {
    public init(rawValue: Int)

    public static var Pickup: FulfillmentOption { get }
    public static var Shipping: FulfillmentOption { get }
    public static var UserShipping: FulfillmentOption { get }
}

More Information can be found in the "Using Swift with Cocoa and Objective-C" reference:

  • "Interaction with C APIs":

Swift also imports C-style enumerations marked with the NS_OPTIONS macro as a Swift option set. Option sets behave similarly to imported enumerations by truncating their prefixes to option value names.

  • "Swift and Objective-C in the Same Project":

You’ll have access to anything within a class or protocol that’s marked with the @objc attribute as long as it’s compatible with Objective-C. This excludes Swift-only features such as those listed here:

  • ...
  • Structures defined in Swift
  • ...
3
Heath Borders On

Objective-C can't see structs, but luckily, you can implement OptionSet with an @objc class instead.

Note that it is very important to implement -hash and -isEqual: because lots of the SetAlgebra default implementations that OptionSet inherits rely on them to work. If you don't implement them, print("\(Ability(.canRead) == Ability(.canRead))") prints false.

@objc
public final class Ability: NSObject, OptionSet {
    // Don't use
    //
    // public static let canRead = Ability(rawValue: 1 << 0)
    // public static let canWrite = Ability(rawValue: 1 << 1)
    //
    // because `Ability` is a `class`, not a `struct`, 
    // so `let` doesn't enforce immutability, and thus
    //
    // Ability.canRead.formUnion(Ability.canWrite)
    //
    // would break `Ability.canRead` for everyone!

    public static func canRead(): Ability { 
        return Ability(rawValue: 1 << 0)
    }

    public static func canWrite(): Ability {
        return Ability(rawValue: 1 << 1)
    }

    public var rawValue: Int

    public typealias RawValue = Int

    public override convenience init() {
        self.init(rawValue: 0)
    }

    public init(rawValue: Int) {
        self.rawValue = rawValue

        super.init()
    }

    /// Must expose this to Objective-C manually because
    /// OptionSet.init(_: Sequence) isn't visible to Objective-C
    /// Because Sequence isn't an Objective-C-visible type
    @objc
    @available(swift, obsoleted: 1.0)
    public convenience init(abilitiesToUnion: [Ability]) {
        self.init(abilitiesToUnion)
    }    

    // MARK: NSObject

    // Note that it is very important to implement -hash and
    // -isEqual: because lots of the `SetAlgebra`
    // default implementations that `OptionSet` inherits 
    // rely on them to work. If you don't implement them,
    // print("\(Ability(.canRead) == Ability(.canRead))")
    // prints `false`

    public override var hash: Int {
        return rawValue
    }

    public override func isEqual(_ object: Any?) -> Bool {
        guard let that = object as? Ability else {
            return false
        }

        return rawValue == that.rawValue
    }

    // MARK: OptionSet

    public func formUnion(_ other: Ability) {
        rawValue = rawValue | other.rawValue
    }

    public func formIntersection(_ other: Ability) {
        rawValue = rawValue & other.rawValue
    }

    public func formSymmetricDifference(_ other: Ability) {
        rawValue = rawValue ^ other.rawValue
    }  
}

Then, I can instantiate this from Objective-C:

Ability *emptyAbility = [Ability new];
Ability *readOnlyAbility = [[Ability alloc] initWithAbilitiesToUnion:@[Ability.canRead]];
Ability *readWriteAbility = [[Ability alloc] initWithAbilitiesToUnion:@[Ability.canRead, Ability.canWrite]];