1

I have IKabaSDK.h which is a (Objective-C) protocol :

@import Foundation;
@import MobileSdk;
    
NS_ASSUME_NONNULL_BEGIN
    
@protocol IKabaSDK <NSObject>
     
- (BOOL)isStarted:(NSError* _Nullable __autoreleasing * _Nullable)error
__attribute__((swift_error(nonnull_error)))
NS_SWIFT_NAME(isStarted());

I have a (Swift) class KabaSDKThunk where the said protocol is implemented:

class KabaSDKThunk: NSObject, IKabaSDK {
    func isStarted() throws -> Bool {
        do {
            try sdk.isStarted()
            print("thunk getIsStarted")
        } catch {
            print("thunk getIsStarted throws \(error)")
            throw error
        }
    }
    ...
}

Xcode gives me these two errors next to my implementation:

  • Non-@objc method isStarted() does not satisfy requirement of @objc protocol IKabaSDK
  • Throwing method cannot be an implementation of an @objc requirement because it returns a value of type Bool; return Void or a type that bridges to an Objective-C class

And If I don't add Bool it complains that KabaSDKThunk does not conform to protocol IKabaSDK and suggests to add the Bool return type, so the errors are mutually exclusive. How do I solve the issue and why do we need Bool and throw here?

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49
niagara
  • 33
  • 4
  • Read through the full question and full answer at https://stackoverflow.com/questions/45565960/ns-refined-for-swift-and-return-value for a good explanation. – HangarRash Jan 13 '23 at 22:29
  • @HangarRash if you feel like another question/answer has already covered the situation OP describes, flag it as duplicate with the reference to the said question/answer – The Dreams Wind Jan 14 '23 at 12:59
  • 1
    @TheDreamsWind I chose not to flag it as a duplicate because the one I linked is more the opposite of what is being asked even though the answer is broad enough to cover both cases. Curious why you suggest that I flag it as a duplicate yet you chose to post an answer instead of flagging as a duplicate. – HangarRash Jan 14 '23 at 14:57
  • @HangarRash I personally don't think the question is a duplicate AND we are not supposed to think the same. My point was to give advise, that leaving a comment to a possible duplicate leaves the question in an undetermined state, while flagging as duplicate can potentially help to come to final conclusion and has better value for the community. – The Dreams Wind Jan 14 '23 at 16:01

1 Answers1

2

why do we need Bool and throw

This question boils down to what you in fact want to achieve. I agree the given behaviour is inconsistent, and moreover, the Swift bridging rules were changed in the past and is likely a subject to change in future, so what you have currently broken, can turn into working sample in foreseeable future. However for now, this error says it all:

Throwing method cannot be an implementation of an @objc requirement because it returns a value of type Bool; return Void or a type that bridges to an Objective-C class

In other words, if your Swift method is marked with throws keyword (and exposed to Objective-C runtime), it has to return a plain Objective-C type (not magically bridged Swift structures, like Int, Bool, Double, etc.. it has to be something subclassed from NSObject, NSProxy or other Objective-C entities) OR return nothing. Period. Take it as a rule (at least for now). (P.S. This specific situation indeed looks like an LLVM bug, because the same set of requirements works perfectly fine when applied to a non-protocol Objective-C method (submitted it here, so the community has a chance to review it))

Having that said, the proper workaround to this situation depends on your final goal.

"Conventional" failable method

By "conventional" here I mean a contract that Cocoa/Cocoa touch programmer would usually expect. In this scenario a method with the following signature:

- (BOOL)failableMethodWithError:(NSError **)error;

Is commonly meant to fail wherever it returns NO value (in the world of Objective-C). It's uncommon for Swift to deal with indirect parameters like NSError ** so in order to keep it consistent such methods are bridged as follows:

func failableMethod() throws

You can read more about this convention in the About Imported Cocoa Error Parameters documentation.

Failable method with preserved return type with swift_error(nonnull_error) attribute

If you want to preserve return type AND the failable signature, you have two options. First is by giving the method swift_error(nonnull_error) attribute. However in this case, in order to comply with existing bridging rules, your method has to have a type that "bridges" to an Objective-C class, e.g. NSNumber *:

- (NSNumber *)failableMethodWithError:(NSError ** _Nullable)error __attribute__((swift_error(nonnull_error)));

And here is how you implement such a method in Swift:

func failableMethod() throws -> NSNumber {
    return NSNumber(booleanLiteral: false)
}

Failable method with preserved return type with swift_error(none) attribute

Another option to preserve the return type is by disabling Objective-C - Swift error signature conversion at all with use of swift_error(none) attribute:

- (BOOL)failableMethodWithError:(NSError ** _Nullable)error __attribute__((swift_error(none)));

In this scenario you can return a scalar type from the failable method, but at the same time you will have to deal with NSErrorPointer type:

func failableMethodWithError(_ error: NSErrorPointer) -> Bool {
    if (/*error condition */) {
        error?.pointee = NSError(domain: TDWErrorDomain, code: TDWErrorDomainErrorCode)
    }
    return true
}
The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49