Check for non-optional values for nil in Swift
Asked Answered
A

4

5

In Swift, it's rare but possible to end up with a value of a non-optional type that has a nil value. As explained in answers to this question, this can be caused by bad Objective-C code bridged to Swift:

- (NSObject * _Nonnull)someObject {
    return nil;
}

Or by bad Swift code:

class C {}
let x: C? = nil
let y: C = unsafeBitCast(x, to: C.self)

In practice, I've run into this with the MFMailComposeViewController API in MessageUI. The following creates a non-optional MFMailComposeViewController, but if the user has not set up an email account in Mail, the following code crashes with EXC_BAD_ACCESS:

let mailComposeViewController = MFMailComposeViewController()
print("\(mailComposeViewController)")

The debugger shows the value of mailComposeViewController like this:

mailComposeViewController = (MFMailComposeViewController) 0x0000000000000000

I have a couple of questions here:

  1. I note that the documentation for unsafeBitCast(_:to:) says it "breaks the guarantees of the Swift type system," but is there a place in Swift documentation that explains that these guarantees can be broken, and how/when?
  2. What's the idiomatic way to check for this case? The compiler won't let me check whether mailComposeViewController == nil since it's not optional.
Amaris answered 30/10, 2017 at 15:45 Comment(1)
The obvious solution for the mail composer is to first check if email can be sent.Thunderous
U
6

Even Apple's APIs sometimes return nil for a type that is not marked in the API as Optional. The solution is to assign to an Optional.

For example, for a while traitCollectionDidChange returned a UITraitCollection even though it could in fact be nil. You couldn't check it for nil because Swift won't let you check a non-Optional for nil.

The workaround was to assign the returned value immediately to a UITraitCollection? and check that for nil. That sort of thing should work for whatever your use case is as well (though your mail example is not a use case, because you're doing it wrong from the get-go).

Unexceptionable answered 30/10, 2017 at 16:17 Comment(1)
I met such a case, what should I do? Write to Apple to open a bug? In my case it clearly looks like someone forgot the question mark :)Hortative
C
1

There is probably a better way to do this with actual Swift pointer types, but one option might be to just peek at the address:

func isNull(_ obj: AnyObject) -> Bool {
    let address = unsafeBitCast(obj, to: Int.self)
    return address == 0x0
}

(Int is documented as machine word sized.)

Cogitative answered 30/10, 2017 at 16:11 Comment(1)
This isn't quite general-purpose: I'm not sure how to do this with value types, which includes, for example, an NSArray` that gets automatically bridged as [Any].Cogitative
L
0

I have same issue but I use suggestion from the Apple docs.

From the documentation:

Before presenting the mail compose view controller, always call the the canSendMail() method to see if the current device is configured to send email. If the user’s device is not set up for the delivery of email, you can notify the user or simply disable the email dispatch features in your application. You should not attempt to use this interface if the canSendMail() method returns false.

if !MFMailComposeViewController.canSendMail() {
    print("Mail services are not available")
    return
}

Considering usage of the unsafeBitCast(_:to:).

The main problem is that in this method we cast a pointer to a value from Swift without any validation if that pointer can conform to the type we are expecting, and from my point of view it is quite dangerous because it will produce crash.

Lookin answered 30/10, 2017 at 15:51 Comment(0)
M
0

Another approach would be to declare a function similar to this:

@inline(never) func isNil<T>(value: T?) -> Bool {
    value == nil
}

Without the @inline(never) you are at risk in Release builds, as the compiler can choose to inline the function body, and afterwards see that value == nil can, theoretically, never be true, thus also removing the check, making the production code behave like you would've never written the code for checking against nils.

Marjorie answered 29/11, 2022 at 17:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.