May 4, 2020

Xcode 11 run app on iOS 12 and lower

Xcode 11 run app on iOS 12 and lower

Creating an app with Xcode 11 has a little bit of a hidden issue, it does not allow you to run the app on iOS 12 or lower out of the box. There are a few changes that need to be made in order to get your app to work on the older versions of iOS.

In this tutorial I will show you how you can get your app that was created with Xcode 11 to run on iOS 12 and lower.

How to get the error

Recently I created an empty project that I wanted to get it running on iOS 11, but, because the app was created with Xcode 11, I had to go into the General settings and change my deployment target from iOS 13 down to iOS 11.

When I tried to run the app I was greeted with a bunch of wonderful errors which you can see below:

Given the title of the article, I am assuming you have this issue too. It confused me for a few minutes, but when I looked at the errors everything seemed to be related to UIScene in some form or another.

Anything below iOS 13 cannot use UIScene because it is only available for iOS 13 and newer. So, the fix is quite simple, all UIScene code needs to be updated so that it is only available for iOS 13.

Step 1: Update SceneDelegate.swift

The first and most obvious file that we need to look at is SceneDelegate.swift. All we need to do here is add @available(iOS 13.0, *) before the class declaration.

Add the available attribute to the SceneDelegate class like below:

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate

If we build the app after adding this we can see that we only have 6 more errors!

Step 2: Update AppDelegate.swift

Now that we know how we fixed the SceneDelegate, we can apply the same fix to AppDelegate.

There are two methods in the AppDelegate that need to be fixed. The below methods need to be updated:

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}

After updating with @available(iOS 13.0, *) attribute, they should look like this:

@available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

@available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}

The app will build now! But, it does not work as expected:

When building and running on a device that is using a version of iOS that is lower than iOS 13, it will result in a black screen. This is because there is no window available in AppDelegate. If you look at the SceneDelegate, it has a window variable which is why this will work in iOS 13.

So, we need to update AppDelegate by adding a window variable. Add the following code above the didFinishLaunchingWithOptions method:

var window: UIWindow?

Awesome! If we build and run the app now, everything should be working as expected. I have added a label to my Main.storyboard just to show that this is working on iOS 11 and iOS 13:

Left image is iOS 13, right image is iOS 11

Conclusion

It is unfortunate that we need to make these changes in order for apps that have been created with Xcode 11 to run on iOS versions lower than iOS 13. It is often the case that one wants to support the current OS as well as two previous OS versions, for eg, iOS 13, 12 and 11.

I am not sure why Apple couldn't add the @available attribute automatically and only remove it once iOS 15 comes out. But, at least the fix is easy!

If you want to see the full source code of this tutorial, you can find it here.