fabián cañas

Optionals in Swift Objective-C Interoperability

2020-01-09

The following observations were made with Swift 5.1.3 and Xcode 11.3. It’s my hope that the contents of this page can be made irrelevant in the near future.

I want to show you something sneaky that can happen when mixing Objective-C and Swift code. Let’s say we have a class written in Objective-C with the following interface:

@interface SomeThing : NSObject
@property (nonatomic,nonnull) NSScrollView *scrollView;
@end

But there’s a problem with the implementation. It’s empty. Given the following implementation, a new instance of SomeThing will have a nil scroll view property, in violation of its interface. The compiler does not complain.

@implementation SomeThing
@end

The scroll view property, which should be nonnull, or in Swift, not optional, is never given a value on initialization. So what happens when we use if from Swift?

let thing: SomeThing = SomeThing()
let scrollView: NSScrollView = thing.scrollView

I’ve explicitly added types to these variables to show that they’re not optional. Those explicit types are the types that are inferred by the compiler if they weren’t there. Swift doesn’t think they’re optional, and doesn’t treat them as optional in any way.

What do you think will happen when the program runs?


Absolutely nothing.

Most people expect it would crash. It doesn’t crash. It instantiates a SomeThing into thing. Then thing’s “scroll view” is read and put into scrollView, and exits without any problems at all.

Let’s go a little crazy…

let thing: SomeThing = SomeThing()
let scrollView: NSScrollView = thing.scrollView

let contentSize: CGSize = scrollView.contentSize
// ^ this is now a 0 width, 0 height size.

let borderType: NSBorderType = scrollView.borderType

switch borderType {
case .noBorder:
    print("no border") // <- This one prints
case .lineBorder:
    print("line border")
case .bezelBorder:
    print("bezel border")
case .grooveBorder:
    print("groove border")
@unknown default:
    print("unknown border")
}

scrollView.flashScrollers()
// glad we could get that out of the way...
// The scrollers have been flashed. Right?

// getting a nonnull property out...
let clipView: NSClipView = scrollView.contentView
// No problem...
clipView.backgroundColor = NSColor.blue

All of that code runs without a problem at all.

Lines 1-20 show methods that return sized structs, like CGSize, or C primitives like floats, ints, and enums with those types, Swift will return a perfectly valid zero-filled value.

Line 22 shows that calling a method with no return value is perfectly valid.

Line 27 shows that a nonnull return value on the nil scrollView is subject to the same problem at the root of all of this. clipView is a non-optional reference to an NSClipView object. But it’s actually nil.

And line 29, setting a value on the nil clipView, rounds out the typical operations we may want to do on an object. Swift doesn’t complain at all about setting a value on a nil objects. This shouldn’t be surprising since for Objective-C objects, it’s the same as calling a method.

Any Objective-C things we want to do with these objects succeeds, which is nearly everything since they’re Objective-C objects. We’ve entered the territory of undefined behavior. It’s a sort of “Objective-C mode”.

There are things we can do to detect this non-optional nil condition. Indeed, the simplest way to get this to fail is to print the unexpectedly nil object. But maybe we can do better than that.

Detecting nil when nil isn’t possible

Say we want to guard against this. The problem is that since Swift doesn’t think this value can be nil, it’s not trivial to check. Comparing the non-optional value results in a warning: “Non-optional expression of type ‘NSSScrollView’ used in a check for optionals”

let thing: SomeThing = SomeThing()
guard let scrollView: NSScrollView = thing.scrollView else {
    struct AnonymousError: Error {}
    throw AnonymousError()
}
Non-optional expression of type ‘NSSScrollView’ used in a check for optionals

Or a slightly different error around conditional binding “Initializer for conditional binding must have optional type, not ‘NSScrollView’.”

let thing: SomeThing = SomeThing()
guard let scrollView = thing.scrollView else {
    struct AnonymousError: Error {}
    throw AnonymousError()
}
Initializer for conditional binding must have optional type, not ‘NSScrollView’.

Or if we try and compare the value directly, something a little more interesting happens:

let thing: SomeThing = SomeThing()
let scrollView = thing.scrollView
if scrollView == nil {
    print("The compiler says we won't get here.")
    print("But if we run the program, we do")
}
Comparing non-optional value of type ‘NSScrollView’ to ‘nil’ always returns false

It says the non-optional value shouldn’t be compared to nil, and that it’s always false. But at run time, the nil is detected, and we print the statement.

None of these are ideal since there are warnings. Can we write a function to erase the non-null nature of this variable?

Well… not like this:

func isNil(_ o: Any?) -> Bool {
    switch o {
    case .none:
        return true
    case .some(_):
        return false
    }
}

if isNil(scrollView) {
    print("This doesn't print.")
}

However if the function takes an AnyObject? parameter, it does have the intended effect:

func isNil(_ o: AnyObject?) -> Bool {
    switch o {
    case .none:
        return true
    case .some(_):
        return false
    }
}

if isNil(scrollView) {
    print("It works if we make it an AnyObject?")
}

We could use a function like the second isNil to detect, assert, and adapt our logic around this unusual case.

Swift Extensions

If you make a Swift extension to the Objective-C class and call them on one of these nil things that aren’t supposed to exist, those methods still get called.

Still working with out unexpectedly nil scroll view instance, we can do something like this:

extension NSScrollView {
    func doAThing() {
        print("doing it") // <- This will get called
    }
}

In those circumstances, you can be inside an instance method of an object and have self be 0x0. And since the type on self is not optional, the silent unexpected nil values can continue to propagate.

Extension methods open up the opportunity for new unexpected behavior though. They execute code instead of running in an unusual “Objective-C” mode where messaging nil returns zero-like values or no-ops. So extension methods can have side-effects like the print statement above. And they can also return non-zero values:

extension NSScrollView {
    func oneHundred() -> Float {
        return 100 // <- Now scrollView.oneHundred() can return 100
    }
}

Foundation Objects

Something really interesting happens with Foundation objects. NSCalendar is a class in Foundation. So if we make a class like this, again with an invalid empty implementation:

@interface CalendarProvider : NSObject
@property (nonatomic, nonnull) NSCalendar *calendar;
@end

And try and use it from Swift like this:

let calendarProvider = CalendarProvider()
let calendar = calendarProvider.calendar
let weekStartsOn = calendar.firstWeekday
let weekdays: [String] = calendar.weekdaySymbols

We might expect the same bad behavior we’ve been exploring so far.

But that’s not what we find.

The program crashes on the second ine. This happens because an Objective-C NSCalendar is a Calendar in Swift. But this isn’t just a rename. It’s bridged to the Swift Foundation Calendar type.

What’s happening here isn’t that we crash when getting an unexpected value out of calendarProvider, but that Swift automatically converts any instances of NSCalendar objects with a C or Objective-C implementation with a Calendar object from the Swift Foundation library. The Swift Foundation library’s Calendar has a method _unconditionallyBridgeFromObjectiveC that’s part of the _ObjectiveCBridgeable protocol that converts an Optional<NSCalendar> to a Foundation.Calendar. we can look at the source for Calendar._unconditionallyBridgeFromObjectiveC.

public static func _unconditionallyBridgeFromObjectiveC(_ source: NSCalendar?) -> Calendar {
    var result: Calendar? = nil
    _forceBridgeFromObjectiveC(source!, result: &result)
    return result!
}

What’s interesting here is that the argument to the bridge function is an Optional<NSCalendar>. The static method, by its signature, accepts nil. What’s happening then? In this case, The culprit for the crash and what saves us from unexpected behavior later on is a force unwrap. Though the value that’s actually passed in to the function is Optional<NSCalendar>.some(nil), which is still not a valid value and we’re still in undefined behavior territory, so it’s pleasantly surprising that a force unwrap catches this case.

Array Properties

Nonnull array properties in Objective-C get bridged to Swift in a very strange way. The following Objective-C class declared a public nonnull NSArray property.

@interface OffendingObject : NSObject
@property (nonnull) NSArray *array;
@end

A description method is added here to illustrate the state of the object later.

@implementation OffendingObject

- (NSString *)description
{
    return [NSString stringWithFormat:
    @"%@"
    "array: %@",
    [super description],
            self.array];
}

@end

What happens when we try and interact with an offending object with the following Swift program? Remember that the object’s array is bridged to a Swift Array property.

let obj = OffendingObject()
print(obj)
print(obj.array)
print(obj)
obj.array.append("thing")
print(obj)

Run it, and you get this output.

<OffendingObject: 0x1007b0380>(
	array: (null)
)
[]
<OffendingObject: 0x1007b0380>(
	array: (null)
)
<OffendingObject: 0x1007b0380>(
	array: (
	thing
)
)

There are at least two very strange things going on in this program. Lines 1 and 2 are uneventful. We can instantiate an OffendingObject, and print its description. Its array property is appropriately is represented by the description Objective-C give nil in a format string, “(null)”.

In line 3, strange things start to happen. print(obj.array) accesses the array property in Swift. That expression should result in nil, which should be a bit of a problem for Swift. Instead, it describes an empty array, as if there were no contractual violation by the OffendingObject at all. If we store the value of obj.array at that point, we do indeed get an empty Array<Any>. It suggests what happens next, which gets really interesting.

We check the object’s property again in line 4 by printing the description of the object. Its array property is still nil.

This situation doesn’t look self-consistent. Under some conditions, Swift will create an Array if it doesn’t find one where it’s expected.

In line 5, we add a string, “thing” to the object’s array. Somewhat surprisingly, that doesn’t crash. And the OffendingObject ends up with an array seemingly conjured out of nowhere that contains a value.

Swift Arrays and Objective-C arrays are funny though. If we set aside where the new empty array comes from for a moment, the rest of the behavior we’ve seen with arrays makes sense. An NSArray can’t be mutated. But swift arrays are different. Semantically, they are value types, not reference types. So changing a var example: Array by adding a new element is the same as creating a new array and assigning it back to the var example container. It’s the variable example that changes, not the Array.

The reason the Swift code to “add to” that array works is because the OffendingObject is compatible with those operations. NSArray isn’t mutable, but the array property is readwrite. So the code is getting the array in the property, creating a new array with those contents plus a new object, then storing that new array back into the OffendingObject’s property.

That just leaves the question of where that empty Array is coming from. Like NSCalendar above, NSArray is bridged to a Swift Array, and we can look at the implementation for _unconditionallyBridgeFromObjectiveC directly.

static public func _unconditionallyBridgeFromObjectiveC(_ source: NSArray?) -> Array {
    if let object = source {
        var value: Array<Element>?
        _conditionallyBridgeFromObjectiveC(object, result: &value)
        return value!
    } else {
        return Array<Element>()
    }
}

This code is doing something very similar to our isNil function above. The if let check on line 2 correctly identifies the nil value, we jump to line 7, where a new empty Array is created and returned.

What do we do now?

Swift is an incredible language. It’s in the top few of my favorite languages, and one of the languages I know best. I delighted in finding this issue and digging into it because I love a good mystery. And I also see this sort of issue as one of the growing pains of any language. Swift is growing up, and it’s going to develop quirks and gotchas, and language features people are strongly advised against using. That’s delightful.

That’s how I felt early in the investigation. My feelings towards the language haven’t changed, but I see this behavior as hugely problematic. Swift is breaking what, in my mind, were the two biggest promises it was making:

  1. The safety afforded by a strong type system
  2. Stellar Objective-C interoperability

The good news is that the Swift team knows about the issue: SR-8622 and SR-120.

The bad news is currently medium priority and has been known since mid-2018, or earlier depending on how you read it.

The other good news is that a reasonable solution has at least been mentioned in SR-8622:

The cost of checking every nonnull return value was determined to be too high, but maybe we could do it in Debug builds.

—Jordan Rose, SR-8622

Having the the compiler automatically check and assert that nonnull Objective-C types returned by Objective-C methods are indeed present would be fantastic, whether for debug builds or as an independent flag. I hope Apple, or some Swift open source contributor can make that happen soon.

In the mean time, I’m not going to be using Swift any less. It’s important to know where the sharp edges are.