UIApplication.shared is undefined during app launch, but it works!

So do the following. We're only concerned about the init statement here, body is standard and unimportant.

@main
struct SomeApp: App {
    init() {
        print(UIApplication.shared) // Crash = EXC_BAD_ACCESS (code=1, address=0x0)
    }
    var body: some Scene {
        ...
    }
}

App crashes at the print statement. Okay. Fine.

Now try this...

@main
struct SomeApp: App {
    init() {
        print(UIApplication.shared.isNetworkActivityIndicatorVisible) // prints false!!!
    }
    var body: some Scene {
        ...
    }
}

The app DOESN'T crash and the print statement prints false.

Breakpoint the print statement and check in the debugger....

(lldb) po UIApplication.shared
 <uninitialized>
(lldb) po UIApplication.shared.isNetworkActivityIndicatorVisible
false

Can someone explain the black magic that seems to be going on here??? Does dynamic dispatch have some sort of secret understanding with UIApplication???

This was found when discussing an issue with Factory: Crash when using singleton · Issue #30 · hmlongco/Factory · GitHub

UIApplication.shared is declared to return UIApplication, so if you're accessing that value in a context where it's actually giving you a nil reference from the UIKit side, that's going to cause problems for Swift (because on the Swift side of things, we won't be performing any nil checks).

But UIApplication is also an Objective-C class, and so an access like UIApplication.shared.isNetworkActivityIndicatorVisible is going to become an Objective-C message send to UIApplication.shared. It's perfectly valid in Objective-C to send messages to nil, and you'll get back 'reasonable' values for many return types (such as false for Bool values). More info here.

7 Likes

Relevant quote: " Note: If you expect a return value from a message sent to nil , the return value will be nil for object return types, 0 for numeric types, and NO for BOOL types. Returned structures have all members initialized to zero."

I knew this in another lifetime, but I've lived in Swift for so long that my Objective-C-fu is weak.

Thanks!

3 Likes

Working in a mixed Objective-C/Swift codebase for long enough will give you a strong spidey-sense that whenever you see EXC_BAD_ACCESS (code=1, address=0x0000000000000xx) (i.e., any small address) it's probably because you've returned a nil object from Objective-C land to a Swift API that was marked as non-optional. :smile:

1 Like

The root of the problem is that:

  1. either "sharedApplication" shall never be nil, even that early during the app startup, or
  2. sharedApplication shall be annotated with "nullable".

This is what it currently is:

@property(class, nonatomic, readonly) UIApplication *sharedApplication

within "NS_ASSUME_NONNULL_BEGIN" brackets. Which leads to this weird situation:

1 Like

And which can lead to other strange bugs if you assume that the methods or functions you call are in fact working and returning correct values.

I think this would be worth feedback to Apple to add documentation that it's not valid to call UIApplication.shared prior to UIApplicationMain (which SwiftUI calls on your behalf after App.init is called).

And maybe separately, documentation that App.init is called before UIApplicationMain or NSApplicationMain.

5 Likes

Yeah this seems like a very subtle and crucial detail to be aware of.

2 Likes

It has, at least, been true for most, if not all, of UIKit's lifetime. However prior to the SwiftUI app lifecycle support, it was not as easy to accidentally call into UIApplication.shared prior to UIApplicationMain.

1 Like

Yeah, this is the part that's surprising to me. "No UIApplication.shared before UIApplicationMain" makes sense.

3 Likes