Return a BOOL value and throw an error, and convert this nicely to Swift

We have the (usual?) problem that we need a method that returns a BOOL and can throw an error, inside a Framework that can be used from Objective-C and from Swift. Is there a nice and for the user understandable way to do that?


More detail:

If I just needed a method that returns a BOOL, that would look like this:

- (BOOL)isDone


Now (as you, who will hopefully answer this question, will know) if this should return an error at the same time, I can't do it like this, as in:

- (BOOL)isDone:(NSError**)error

the return value tells the user if there was an error, but doesn't return any value.


So the usual ways I have found so far to do this are these:

Send the return value as a pointer and fill it:

-(BOOL)getIsDone:(BOOL*)isDone error:(NSError**)error


or: Return an NSNumber that is nil if there is an error or contains a BOOL if not.

-(NSNumber*)isDone:(NSError**)error


In Objective-C, we so far used the first solution for this. But now the framework needs to nicely map to a swift funtion. Now in Swift, the first solution would map to:

func getIsDone(isDone: UnsafeMutablePointer<ObjCBool>) throws

and the second one to:

func isDone() throws -> NSNumber

which is really unclear for the user why this is an NSNumber and he has to use .boolValue to actually use it...


As you can see, both variants are really bad. What I would prefer is something like:

func isDone() throws -> Bool

or maybe

func getIsDone(isDone: inout Bool) throws


Is there a way to tell the clang compiler to convert it to something like this? Pleeease?

Thanks!

Answered by QuinceyMorris in 278747022

The deeper problem is that the Obj-C pattern for this is already ugly:


-(BOOL)getIsDone:(BOOL*)isDone error:(NSError**)error


and that pattern means you're dealing with a "BOOL*", not a "BOOL". The fact that you're dealing with a pure C pointer (not Obj-C) translates the ugliness into Swift, too.


If you want to pursue a 2-framework solution, one choice would be to distribute the Swift "wrapper" framework (which would be very tiny) in source form. In essence, that's the only alternative that any current Swift developer has, to distributing multiple ABI-versioned binary frameworks.


There might be another solution without a 2nd framework, where you return a result that's an optional Obj-C object of a custom class declared in the Obj-C framework (instead of returning an optional NSNumber). Clients would still have to use a property of the object (e.g. "result.isDone") but that's slightly better than "result.boolValue". In such a case, I wouldn't call the method "isDone ()" or "getIsDone (…)", but something like "status ()".


The other thing that occurs to me is that the right answer depends on what your "isDone" method is for. If you're retrieving a completion status/error for an action that was started earlier, it's not quite right for the method to throw. After all, the "isDone" method itself does not fail in that case. Or, if the method itself does something that really can fail, then maybe split it into two methods, one that tries the failable action, and a second (non-throwing) method that retrieves the "done" status.

You might want to ask this over at swift.org on the swift-users list, since there might be a standard way of handling this, that's easier than the following suggestion.


One approach might be to annotate the Obj-C function with "NS_REFINED_FOR_SWIFT":


https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html


(under the heading "Refining Objective-C Declarations"). This allows you to obscure the Obj-C method behind a different Swift name, and write a nicer Swift replacement that calls the ugly version. In this case, I would suggest using the most obvious Obj-C form:


-(BOOL)getIsDone:(BOOL*)isDone error:(NSError**)error


and make its replacement the most obvious Swift form:


func isDone () throws -> Bool


The trick is that you would want to put the Swift function (in an extension) in the framework with the Obj-C code. According to the instructions on the page linked above (under the heading "Importing Code from Within the Same Framework Target"), that requires making the Obj-C method public, which means it's visible outsidle the framework as well as within it. However, that shouldn't be a big problem, because it wouldn't be wrong for a Swift client of the framework to use the Obj-C version directly, just uglier. You can, in fact, see this sort of thing going on in Apple's SDKs, where you'll see awkard methods showing up in Swift generated interfaces as names starting with "__".

Thank you very much for your answer!


I have read about NS_REFINED_FOR_SWIFT before and automatically assumed that it will break Swift ABI compatibility. Just now I did a test to confirm that I was right (and I really hoped I would be wrong!!!!)


Our first approach to release the framework was actually to create two frameworks: An Objective-C version and a separate Swift version that simply wraps around the Objective-C version and makes the interface nicer for Swift users. But then XCode 9.1 came out and made our Swift wrapper unusable (because Swift version 4.0.2 is not ABI compatible with Swift 4.0). So we decided to drop that approach (as creating and deploying each framework version for each Swift (minor-)version sounds really, really, really horrible) and hoped for something more compatible.


And with NS_SWIFT_NAME and a few tricks, this was almost possible. Except for the problem described above, of course.


So maybe I should refine the question above:

Is there a way to convert an Objective-C function that throws an error and returns a BOOL to something nice in Swift WITHOUT breaking ABI compatibility?

Accepted Answer

The deeper problem is that the Obj-C pattern for this is already ugly:


-(BOOL)getIsDone:(BOOL*)isDone error:(NSError**)error


and that pattern means you're dealing with a "BOOL*", not a "BOOL". The fact that you're dealing with a pure C pointer (not Obj-C) translates the ugliness into Swift, too.


If you want to pursue a 2-framework solution, one choice would be to distribute the Swift "wrapper" framework (which would be very tiny) in source form. In essence, that's the only alternative that any current Swift developer has, to distributing multiple ABI-versioned binary frameworks.


There might be another solution without a 2nd framework, where you return a result that's an optional Obj-C object of a custom class declared in the Obj-C framework (instead of returning an optional NSNumber). Clients would still have to use a property of the object (e.g. "result.isDone") but that's slightly better than "result.boolValue". In such a case, I wouldn't call the method "isDone ()" or "getIsDone (…)", but something like "status ()".


The other thing that occurs to me is that the right answer depends on what your "isDone" method is for. If you're retrieving a completion status/error for an action that was started earlier, it's not quite right for the method to throw. After all, the "isDone" method itself does not fail in that case. Or, if the method itself does something that really can fail, then maybe split it into two methods, one that tries the failable action, and a second (non-throwing) method that retrieves the "done" status.

Thanks for the detailed answer!


Yes, we thought about releasing the wrapper source code, but it's somehow too much work for the customers, so we are checking all other options first. Your first answer gave me the idea that we could simply release the "extension" parts in one file so that the customers can pull these in if they want a nice Swift interface, and can still use the "ugly" Objective-C interface if not (thanks for the idea!).

And "isDone" is just an example :-). We have 8 methods in the framework that have this problem, and some of them depend on device-hardware states, so they can throw exceptions, which we need to catch so that the app will never crash... So at least in some of the 8 cases we need to inform the user if something has failed (but I agree, in some of the cases we could leave the errors away and simply log them...).

This response is a bit late, but here is my solution:

Code Block
-(nullable NSNumber*)isDone:(NSError* _Nullable * _Nullable)error

And then in swift:
Code Block
let result = try obj.isDone().boolValue

Not exactly what you wanted, but close enough IMHO.
Return a BOOL value and throw an error, and convert this nicely to Swift
 
 
Q