Prevent Obj-C code from passing `nil` to a Swift method with non-optional parameters

Viewed 698

I'm dealing with an issue where Obj-C code can pass nil to Swift methods that do not take Optional parameters.

I'm looking for a way for Xcode compiler to prevent this. It seems that all nullability annotations are for Swift to call Objc, not vice-versa.

Some example code:

// file1.swift
@objc
class Foo: NSObject {
  @objc let date: Date? // default to nil
  ...
}

// file2.swift
extension Date {
  @objc(doSomethingWithOtherDate:)
  func doSomething(with otherDate: Date) {
    print(date)
  }
}

Then in Obj-c, we call the Swift method passing in an optional value, with nil:

// file3.m
[Date.new doSomethingWithOtherDate:[Foo.new date]];

Is there a way to prevent this? How can we tell the compiler doSomethingWithOtherDate does not take nil.

Follow up

Following up on this issue, passing a literal nil on the call triggers a warning, which is very helpful. However, passing a nil value does not trigger anything. The static analyzer does however find the issue if the nil reference is local.

However, if a nil reference is passed via another object, the static analyzer fails to identify this issue.

Objc - Swift crash

I've made this repo to showcase this issue: https://github.com/eneko/ObjcCrashDemo

2 Answers

In general, no. Objective-C does not have enough understanding of nullability to detect every case.

If you explicitly pass nil you should get a warning "Null passed to a callee that requires a non-null argument". You can turn this into an error by adding -Werror=nonnull to "Other Warning Flags" in your build settings.

However this is extremely limited. Even something as simple as NSDate* date = nil; and passing that will produce no warning/error. Even adding a nonnull property in Objective-C and passing that will not produce a warning/error.

I was able to find this article: https://fabiancanas.com/blog/2020/1/9/swift-undefined-behavior.html (search for the "Detecting nil when nil isn’t possible" section). This article discusses a rather complex solution to the problem, but I would suggest the easier solution would be to declare your function argument type as Date! (either a value or nil) and then perform a guard let on the passed value before proceeding in the function body:

// file2.swift
extension Date {
  @objc(doSomethingWithOtherDate:)
  func doSomething(with otherDate: Date!) {
    guard let od = otherDate else {
      // do something else
      return
    }
    print(date)
  }
}
Related