Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to call method in Swift class that throws and has non-void return from Obj-C

I want to have a function in Swift that returns a Bool but that can also throw if an exception occurs.

For example:

 func doSomething(value: Int) throws -> Bool {
    if (value > 0) {
        return true
    } else if (value == 0) {
        throw NSError(domain: "SwiftClass", code: 0, userInfo: nil)
    }
    return false
}

This works fine, from Swift, but if I try and use this function from Objective-C, the compiler can't find the method. I know that the throws requires the Objective-C function signature to change to doSomething:x error:&error and this works, if I change the return type to Void -

func doSomething(value: Int) throws -> Void {
    if (value == 0) {
         throw NSError(domain: "SwiftClass", code: 0, userInfo: nil)
    } else if (value < 0) {
        throw NSError(domain: "SwiftClass", code: -1, userInfo: nil)
    }
}

But this has different semantics. In the first example, I only need to deal with the exception (or non-nil NSError) if there is a problem. With this code I have to catch the exception (or examine the error) and determine if it is a real problem or just the valid, "false" case.

Is it really not possible to use a Swift function with a non-void return that throws in an Objective-C context?

like image 540
Paulw11 Avatar asked Apr 17 '16 04:04

Paulw11


1 Answers

As noted in my comments, I'm unclear as to why this shouldn't work. The documentation doesn't give anything to suggest this shouldn't work, but it also doesn't explicitly state that it should. My reading of the documentation says that it should work.

With that said, we could wrap this in a method that is callable from Objective-C by changing the return type to Void and using an inout parameter for the result:

func doSomething(value: Int) throws -> Bool {
    if (value > 0) {
        return true
    } else if (value < 0) {
        return false
    } else {
        throw NSError(domain: "SwiftClass", code: 0, userInfo: nil)
    }
}

func doSomething(value: Int, inout result: Bool) throws {
    do {
        result = try doSomething(value)
    } catch let error {
        // If need be, assign some default value to result.
        throw error
    }
}

Alternatively, as per your comment, it seems the compiler would be happy if we returned a value that can bridge to an Objective-C class (which apparently doesn't include Bool), so we could wrap it as such:

@objc func doSomething(value: Int) throws -> NSNumber {
    do {
        return try doSomething(value)
    } catch let error {
        throw error
    }
}

And in playing around with this, it became clear why this only works when we're returning a value that can map to an Objective-C class.

The compiler will not let you return an optional from a method marked with @objc and throws. Why? Because while on the Swift side, we use try semantics to call the method, the approach is entirely different in Objective-C. nil is used to indicate failure.

So trying to create a method with this signature in Swift:

@objc func doSomething(value: Int) throws -> NSNumber?

Generates this warning:

Throwing method cannot be marked @objc because it returns a value of optional type 'NSNumber?'; 'nil' indicates failure to Objective-C


In the end though, I do still very much recommend that you write the Swift method as having the signature returning Bool and write a wrapper method returning NSNumber so that we still have our Swift method with the most accurate type possible.

Also, if you notice, Bool can happily automatically box up into NSNumber. My wrapper method was written as returning type NSNumber, but the method I'm wrapping returns type Bool, yet Swift was perfectly happy to return Bool from a method that is supposed to return NSNumber. The trouble most likely is that by default, if unspecified, Swift's Bool translates into Objective-C's BOOL, which is a non-class.

Objective-C wouldn't be able to distinguish between the three possible cases if your return type is BOOL. It only has two possible returns from a BOOL method: YES or NO. With Bool mapped to an NSNumber, it can return @YES, @NO, and nil.

And if it's me, NSNumber isn't really type strict enough for me. I might be encouraged to write my own wrapped class for this.

@objc class BooleanObject: NSObject, BooleanType {
    let boolValue: Bool

    init(_ boolValue: Bool) {
        self.boolValue = boolValue
    }
}

Note that the BooleanType protocol means that Swift would still be perfectly happy to use variables of this type as if they were a regular Bool.

let b = BooleanObject(true)

if b {
    // b's boolValue property is true
}
like image 79
nhgrif Avatar answered Nov 15 '22 08:11

nhgrif