swiftobjective-cerror-handlingobjective-c-protocol

Throwing method cannot be an implementation of an @objc requirement because it returns a value of type `Bool`


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?


Solution

  • 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
    }