Using An Objective-C Function that Returns an Optional or Throw in Swift
Asked Answered
I

2

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?

Infraction answered 9/12, 2015 at 19:17 Comment(4)
In your .m file return type is +(NSString *), which isn't _NullableLifeboat
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.Infraction
@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 developer.apple.com/library/ios/documentation/Swift/Conceptual/….Brumbaugh
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?Infraction
P
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.

Pertinacious answered 9/12, 2015 at 20:33 Comment(4)
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.Infraction
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.Pertinacious
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.Infraction
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".Pertinacious
V
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);
}
Vesta answered 10/12, 2015 at 4:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.