How to create class methods that conform to a protocol shared between Swift and Objective-C?
Asked Answered
I

1

11

I've been learning Swift lately.

I decided to write a hybrid Swift/Objective-C app that did compute-intensive tasks using the same algorithm implemented in both languages.

The program calculates a large array of prime numbers.

I defined a protocol that both the Swift and the Objective-C version of the calculate object are supposed to conform to.

The objects are both singletons, so I created a typical singleton access method in Objective-C:

+ (NSObject <CalcPrimesProtocol> *) sharedInstance;

The whole protocol looks like this:

#import <Foundation/Foundation.h>
@class ComputeRecord;

typedef void (^updateDisplayBlock)(void);
typedef void (^calcPrimesCompletionBlock)(void);

    @protocol CalcPrimesProtocol <NSObject>

- (void) calcPrimesWithComputeRecord: (ComputeRecord *) aComputeRecord
              withUpdateDisplayBlock: (updateDisplayBlock) theUpdateDisplayBlock
                  andCompletionBlock: (calcPrimesCompletionBlock) theCalcPrimesCompletionBlock;

    @optional //Without this @optional line, the build fails.
    + (NSObject <CalcPrimesProtocol> *) sharedInstance;

    @end

The Objective-C version of the class implements the methods exactly as defined above, no worries.

The swift version has a method:

  class func sharedInstance() -> CalcPrimesProtocol

However, if I make that method a required method of the protocol, I get a compiler error "Type "CalcPrimesSwift does not conform to the protocol 'CalcPrimesProtocol'.

If I mark the singleton class method sharedInstance as optional in the protocol, however, it works, and I can invoke that method on either my Swift class or my Objective-C class.

Did I miss some subtlety in the definition of my Swift class method? It seems unlikely, given that I'm able to invoke the sharedInstance() class method on either my Swift class or my Objective-C class.

You can download the project from Github and check it out if you'd like. It's called SwiftPerformanceBenchmark. (link)

Insobriety answered 1/4, 2015 at 20:10 Comment(8)
Hmm. A shot in the dark, but should your class func return an AnyObject that conforms to the CalcPrimesProtocol, instead of a protocol object itself?Miele
Thanks for the suggestion. If I try that: class func sharedInstance() -> AnyObject <CalcPrimesProtocol> I get a compiler error "Cannot specialize non-generic type 'AnyObject'".Insobriety
Dang. Maybe I'll clone the project and mess with it.Miele
I'd love to have another pair of eyes. I'm stumped.Insobriety
@nhgrif It's just in Objective-C (I've cloned the project and I checked).Miele
Why would you duplicate the protocol in Swift? The whole point is setting up interpretability so Swift classes/protocols are usable in Obj-C, and visa-versa.Insobriety
Let us continue this discussion in chat.Insobriety
@DuncanC Have you updated to Xcode 6.3 yet? I'm going to update my answer today.Kacey
K
11

In Objective-C, we were always passing around pointers, and pointers could always be nil. Lots of Objective-C programmers made use of the fact that sending a message to nil did nothing and returned 0/nil/NO. Swift handles nil entirely differently. Objects either exist (never nil), or it is unknown whether or not they exist (which is where Swift optionals come into play).

Previous to Xcode 6.3, this therefore meant that any Swift code that used any Objective-C code would have to treat all object references as Swift optionals. Nothing about Objective-C's language rules prevented an object pointer from being nil.

What this meant for using Objective-C protocols, classes, etc., from Swift is that it was a massive mess. We had to choose between to non-perfect solutions.

Given the following Objective-C protocol:

@protocol ObjCProtocol <NSObject>

@required + (id<ObjCProtocol>)classMethod;
@required - (id<ObjCProtocol>)instanceMethod;
@required - (void)methodWithArgs:(NSObject *)args;

@end

We can either accept the method definition as containing implicitly unwrapped optionals:

class MyClass: NSObject, ObjCProtocol {
    func methodWithArgs(args: NSObject!) {
        // do stuff with args
    }
}

This makes the resulting code cleaner (we never have to unwrap within the body), however we will always be at risk of the "found nil while unwrapping optional" error.

Or alternatively, we can define the method as being a true optional:

class MyClass: NSObject, ObjCProtocol {
    func methodWithArgs(args: NSObject?) {
        // unwrap do stuff with args
    }
}

But this leaves us with a lot of mess unwrapping code.

Xcode 6.3 fixes this problem and adds "Nullability Annotations" for Objective-C code.

The two newly introduced keywords are nullable and nonnull. These are used in the same place you're declaring the return type or parameter type for your Objective-C code.

- (void)methodThatTakesNullableOrOptionalTypeParameter:(nullable NSObject *)parameter;
- (void)methodThatTakesNonnullNonOptionalTypeParameter:(nonnull NSObject *)parameter;
- (nullable NSObject *)methodReturningNullableOptionalValue;
- (nonnull NSObject *)methodReturningNonNullNonOptionalValue;

In addition to these two annotation keywords, Xcode 6.3 also introduces a set of macros that makes it easy to mark large sections of Objective-C code as nonnull (files with no annotations at all are effectively assumed as nullable). For this, we use NS_ASSUME_NONNULL_BEGIN at the top of the section and NS_ASSUME_NONNULL_END at the bottom of the section we wish to mark.

So, for example, we could wrap your entire protocol within this macro pair.

NS_ASSUME_NONNULL_BEGIN
@protocol CalcPrimesProtocol <NSObject>

- (void) calcPrimesWithComputeRecord: (ComputeRecord *) aComputeRecord
              withUpdateDisplayBlock: (updateDisplayBlock) theUpdateDisplayBlock
                  andCompletionBlock: (calcPrimesCompletionBlock) theCalcPrimesCompletionBlock;
+ (id <CalcPrimesProtocol> ) sharedInstance;

@end
NS_ASSUME_NONNULL_END

This has the same effect as marking all of the pointer parameters and return types as nonnull (with a few exceptions, as this entry in Apple's Swift blog makes note of).


Pre-Xcode 6.3

A Swift class that conforms to an Objective-C protocol must treat any Objective-C types in that protocol as optionals.

In trying to figure this out, I created the following Objective-C protocol:

@protocol ObjCProtocol <NSObject>

@required + (id<ObjCProtocol>)classMethod;
@required - (id<ObjCProtocol>)instanceMethod;
@required - (void)methodWithArgs:(NSObject *)args;

@end

And then, created a Swift class which inherited from NSObject and declared itself as conforming to this ObjCProtocol.

I then proceeded to type these method names and let Swift autocomplete the methods out for me, and this is what I got (I put in the method bodies, the rest if autocomplete):

class ASwiftClass : NSObject, ObjCProtocol {
    class func classMethod() -> ObjCProtocol! {
        return nil
    }
    
    func instanceMethod() -> ObjCProtocol! {
        return nil
    }
    
    func methodWithArgs(args: NSObject!) {
        // do stuff
    }
}

Now, we can use regular optionals (with the ?) instead of these automatically unwrapped optionals if we want. The compiler is perfectly happy with either. The point is though that we must allow for the possibility of nil, because Objective-C protocols cannot prevent nil from being passed.

If this protocol were implemented in Swift, we'd get to choose whether the return type is optional or not and Swift would prevent us from returning nil to a method that didn't define a non-optional return type.

Kacey answered 1/4, 2015 at 21:49 Comment(4)
Outstanding (voted). Thanks for figuring this out. Time to go change my Swift methods to try to comply with the Obj-C protocol.Insobriety
I guess I spoke too soon. Making the function result an optional of type "conforms to CalcPrimesProtocol" doesn't resolve the problem.Insobriety
The compiler wants me to declare it as class func sharedInstance() -> NSObject!. Then the result doesn't conform to my protocol however.Insobriety
Oh. I see. Can you not declare the protocol's method return type as (id<CalcPrimesProtocol>)sharedInstance;? Is there a specific reason you need NSObject * versus id?Kacey

© 2022 - 2024 — McMap. All rights reserved.