6

I'm working on a swift project that need to interact with an existing objective-c api. I've run into a bit of a roadblock with one of the functions though. In the objective-c header file (OrderItem.h) I have this function definition:

+ (NSString *_Nullable)getOptional:(NSString *_Nonnull)foo error:(NSError *_Nullable *_Nullable)error;

In particular, take notice of the last parameter; because it is an error pointer calls to this method in swift will need to be wrapped in an error hander (do .. catch).

Here is the corresponding .m file:

+ (NSString *)getOptional:(NSString *)foo error:(NSError *__autoreleasing *)error
{
    if([foo isEqualToString:@"abc"])
    {
        return @"abc item";
    }
    else
    {
        if([foo isEqualToString:@"xyz"])
        {
            *error = [[NSError alloc] init];
        }
        return nil;
    }
}

In my swift file I then added this code:

func testGetOptional()
{
    do
    {
        var result:NSString? = try OrderItem.getOptional("abc");
        XCTAssertNotNil(result);
        result = try OrderItem.getOptional("123");
        XCTAssertNil(result);

    }
    catch let error as NSError
    {
        XCTFail("OrderItem lookup should not have thrown an error. Error was " + error.localizedDescription);
    }

}

Nothing especially complicated; neither of the two calls to getOptional should actually result in an error. When I run that function however, the '123' case is blowing up and causing the test to fail. When I took a closer look, it seems that the bridged version of my objective-c is defining the return type as Nonnull (-> OrderItem) even though I explicitly defined it as Nullable in the objective-c. Even more strange, if I declare this same function without the final 'error' parameter then the bridged version will have the correct return type of Nullable (-> OrderItem?).

Can someone shed some light on what is going on here? More importantly, is there some way to work around this issue?

Alex Zavatone
  • 4,106
  • 36
  • 54
pbuchheit
  • 1,371
  • 1
  • 20
  • 47
  • In your `.m` file return type is `+(NSString *)`, which isn't `_Nullable` – Juri Noga Dec 09 '15 at 19:26
  • The return type of the .m file makes no difference, the bridged file used by swift is based on the .h file. Just to be certain, I did try adding _Nullable to the .m but the results were the same. – pbuchheit Dec 09 '15 at 19:38
  • @pbuchheit: That is to be expected. Functions taking an error parameter are translated to Swift as functions throwing an error (and a `NULL` return value indicates an error). See "Error Handling" in https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html. – Martin R Dec 09 '15 at 20:06
  • So is there no way to handle cases where a method could return a value OR throw an error that we need to do something with? – pbuchheit Dec 09 '15 at 20:33

2 Answers2

6

In Cocoa there is the error pattern that a method that can fail will have a return value and an indirect error parameter. If it is a method returning a object reference, the pattern is

  • set the reference the indirect error argument points to,

  • return nil

So it looks like this

+(NSString *) getOptional:( NSString *) foo error:(NSError *__autoreleasing *)error
{
   …
   // Error case
   *error = [NSError …];
   return nil;
   …
 }

In Swift errors are translated into a docatch construct. Therefore a return value of nil signaling an error is never used from the Swift point of view, because the execution is caught. Therefore it is non-nullable.

Amin Negm-Awad
  • 16,582
  • 3
  • 35
  • 50
  • 1
    So how would that pattern work with something like a fetch request? If I'm fetching something then returning null would be possible, even if no errors were thrown, if the fetch simply doesn't bring back any results. – pbuchheit Dec 09 '15 at 20:38
  • When you have a method implementing this error pattern, a return value of `nil` *always* signals an error. If a fetch requests wants to return an empty result set, it returns an empt array, not `nil`. There is a difference between "no collection object" (aka `nil`) and "empty collection object". Null is not zero. – Amin Negm-Awad Dec 09 '15 at 20:44
  • 1
    That is not always the case though. For example, we frequently have fetch methods to get the first item of a fetch result. If the result array is empty, there is no first item so nil is the only return value that makes any sense. – pbuchheit Dec 09 '15 at 20:47
  • I do not know, what makes sense to you. However, the Cocoa error pattern is what it is. There are two usual ways to deal with situations that can have a non-erronous `nil` value: a) Have an out parameter (`-getFirstItem:error:`) for the result and a boolean return value for signaling errors. b) Return a special return value for "no result". – Amin Negm-Awad Dec 09 '15 at 20:52
1

You cannot return nil from a function that uses NSError to indicate a non-error state. If the method return nil, the error pointer must be set.

This will fail when you call it with 123:

// ObjC
+(NSString *) getOptional:( NSString *) foo error:(NSError **)error
{
    if ([foo isEqualToString:@"abc"])
    {
        return @"abc item";
    }
    else if ([foo isEqualToString:@"xyz"])
    {
        NSDictionary * userInfo = @{NSLocalizedDescriptionKey: @"foo cannot be xyz"};
        *error = [NSError errorWithDomain:NSCocoaErrorDomain code:1 userInfo:userInfo];
    }
    else if ([foo isEqualToString:@"123"])
    {
        // 123 is a valid input, but we don't have anything to return
        return nil;
    }

    return nil;
}

// Swift
do {
    let result = try OrderItem.getOptional("123")
} catch let error as NSError {
    print(error.localizedDescription)
}

// Fail:
// The operation couldn’t be completed. (Foundation._GenericObjCError error 0.)

It has no problem if you call it in ObjC, but the bridging to Swift renders it invalid. Instead, you must return a non-nil value that your application will interpret as empty:

// ObjC
else if ([foo isEqualToString:@"123"])
{
    // If it has nothing to return, return an empty string as a token for nothingness
    return [NSString string];
}

// Swift
do {
    let result = try OrderItem.getOptional("123")
    XCTAssert(result.isEmpty, "result must be empty")
} catch let error as NSError {
    XCTFail("OrderItem lookup should not have thrown an error. Error was " + error.localizedDescription);
}
Code Different
  • 90,614
  • 16
  • 144
  • 163