Deadlock in UIKit while injecting test bundle

Platform and Version

  • iOS
  • Development environment: Xcode 16.0 beta 5 (16A5221g), macOS 14.6.1 (23G93)
  • Run-time configuration: iOS 18.0 beta 5 (22A5326g)

Description of Problem

Starting with iOS 18 SDK, test bundles containing a +load method that accesses UIScreen.mainScreen result in deadlock during test bundle injection.

Also filed as FB14703057

I'm looking for clarity on whether this behavior is considered a bug in iOS or whether we will need to change the implementation of our app.

Steps to Reproduce

  1. Create a new iOS app project using Objective-C and including unit tests

  2. Add the following code snippet to any .m file in the test target:

    @interface Foo: NSObject
    @end
    
    @implementation Foo
    + (void)load {
        UIScreen * const mainScreen = UIScreen.mainScreen;
        NSLog(@"%@", mainScreen);
    }
    @end
    
  3. Run the tests

Expected Behavior

As with iOS 17 & Xcode 15, the tests run to completion.

Actual Behavior

With iOS 18 & Xcode 16, deadlock during test bundle injection.

Answered by DTS Engineer in 800861022

I suggest you change your implementation here, as you're relying on initialization details for UIKit objects and how they behave prior to main being run, which as you unfortunately found out here, is subject to change.

Note also that you wouldn't be able to write the same code in Swift — +load in a NSObject defined in Swift won't get called, as noted by the documentation. While you're using this pattern for testing, in a normal launch sequence, a method like this which runs prior to main causes launch time impacts. Often, alternate patterns are available for needs in +load which move the work to after main is called, enabling faster app launch times. Swift removing this paradigm was discussed many WWDCs ago in this context of focusing in on faster launch times. For you and your testing needs, that guidance may not be directly applicable in this scenario, but I hope these notes help you see why you'll need to look for a new approach to meet your test setup needs than this one.

— Ed Ford,  DTS Engineer

Accepted Answer

I suggest you change your implementation here, as you're relying on initialization details for UIKit objects and how they behave prior to main being run, which as you unfortunately found out here, is subject to change.

Note also that you wouldn't be able to write the same code in Swift — +load in a NSObject defined in Swift won't get called, as noted by the documentation. While you're using this pattern for testing, in a normal launch sequence, a method like this which runs prior to main causes launch time impacts. Often, alternate patterns are available for needs in +load which move the work to after main is called, enabling faster app launch times. Swift removing this paradigm was discussed many WWDCs ago in this context of focusing in on faster launch times. For you and your testing needs, that guidance may not be directly applicable in this scenario, but I hope these notes help you see why you'll need to look for a new approach to meet your test setup needs than this one.

— Ed Ford,  DTS Engineer

Is there any documentation that states or implies that using UIKit in +load is unsafe or relies on implementation details that are subject to change?

@Stephan_, not for UIKit specifically, but as I noted in my comment, there is documentation for Foundation that indicates the method is unavailable from Swift. While that isn't exactly what you're looking for, it does outline the direction of the system in that using +load and all of the associated problems and costs it incurs are now intentionally removed by disallowing its use from Swift.

— Ed Ford,  DTS Engineer

Deadlock in UIKit while injecting test bundle
 
 
Q