Dealing with iOS 13 Deprecations

In an earlier article, I talked about the major and minor changes you might expect to grapple with when updating your older app to iOS 13 and iOS 14. In that article, however, I didn’t talk about deprecations. So I want to talk about them now.

What is a deprecation? A deprecation is when [WARNING: Never begin a definition with the words “x is when…!” — my fifth-grade teacher told me not to] Apple decides that some aged piece of the official framework API should no longer be used. It’s still there, but Apple wants you to migrate away from it, as it has been superseded by something newer and better. To deprecate something, in this sense, is quite literally to request that you avoid it (from Latin precor, ‘ask’, and prefix de-, ‘away’, implying negativity).

A deprecation from Apple can also serve as a semi-official signal that, in some future version, this piece of API may be taken away altogether. At that point, using it can result in a compile error or a runtime crash. So it’s better to stop using it now, before that happens.

A classic example is the UIView class method beginAnimations(_:context:) and the whole family of methods such as commitAnimations() that goes with it. Way back in iOS 2 and 3, this was how you asked for a view animation. Starting in iOS 4, it was gradually replaced by animate(withDuration:...), as well as the UIViewPropertyAnimator class which was introduced in iOS 10.

When you look at the documentation for beginAnimations(_:context:), under Availability, it says “Deprecated” in red. If you look in the headers, you’ll see this annotation in the beginAnimations(_:context:) declaration:

API_DEPRECATED("Use the block-based animation API instead", ios(2.0, 13.0));

That summarizes the history. This method was introduced very early, in iOS 2.0. Over the years, it was gradually superseded for nearly all usages; finally, starting in iOS 13, Apple decided that it was time to pull the plug on it and declare it officially deprecated.

Discovering that something has been deprecated, however, is not always easy. First of all, you would obviously need to update Xcode itself to a recent version. Then, clearly, if you happen to look in the documentation or the headers, you can find out that the deprecation has been issued. But how likely are you to do that? It is more probable that you will simply use a deprecated API in your code, and expect the compiler to let you know if there’s an issue.

But if you are merely using a deprecated API in your code, everything depends on your app’s deployment target. If I use beginAnimations(_:context:) in an app project whose deployment target is iOS 12, I won’t discover the deprecation, for the simple reason that in iOS 12 this method is not deprecated. But when I update my app’s deployment target to iOS 13, then when I build my app a compiler warning appears, notifying me of the deprecation. And code completion works the same way; if I type beginAnim and get code completion, if my app’s deployment target is iOS 12 or earlier, I’ll simply see beginAnimations(_:context:), but if my app’s deployment target is iOS 13 or later, those words will have a strikeout through them.

So here’s a tip: even if your app is backward compatible to an older version of iOS, it can be a good idea, now and then, to try compiling it against a more recent deployment target, just in order to get Xcode to signal any upcoming deprecations.

Now, the fact is that you were probably not using beginAnimations(_:context:) and its cohorts in your code in any case; so this is not a deprecation that is likely to affect you. But there are some significant and possibly surprising deprecations that you might encounter when you move your older app’s deployment target up from iOS 12 or earlier to iOS 13 or later. And in this article I just want to mention a few of them. These are actually deprecations that I am very frequently running into during the course of updating my own existing apps, so presumably the same thing might happen to you. Forewarned, as they say, is forearmed.

UIActivityIndicatorView Styles

Maybe I’m just someone who uses a lot of UIActivityIndicatorViews, but all my apps seem to have run headlong into this one. The UIActivityIndicatorView styles, .whiteLarge, .white, and .gray, are all deprecated in iOS 13 and are replaced by .medium and .large.

This isn’t difficult to cope with; just make the necessary change. For instance, all my .whiteLarge activity views are now .large activity views. But you may have a bit more work to do afterward. If your goal is to set the activity view spinner’s color, don’t forget about that; a .largeWhite or .white activity view that has been changed to .large or .medium is no longer white! Use the activity view’s color property to fix that.

Network Activity Indicator

Another sort of activity indicator that I seem to use a great deal is the network activity indicator. This is the little spinner that appears within the status bar, intended to be shown while your app is performing (you guessed it) network activity. To show or hide it, you set UIApplication.shared.isNetworkActivityIndicatorVisible to true or false; if the indicator is visible, it is spinning.

Except that it isn’t going be visible any longer, because we are now told that this property is deprecated in iOS 13. What will you use instead? The deprecation message says: “Provide a custom network activity UI in your app if desired.” That’s not very informative, but I suppose the message is clear: it’s a kiss-off. Apple is telling you that from now on, you’re on your own, and good luck.

An obvious solution would be to use a UIActivityIndicatorView. This, unfortunately, is not as flexible as the true network activity indicator. The great thing about the status bar, after all, is that it is not inside your app’s user interface. (It’s actually a secondary window belonging to the runtime.) This means that it persists regardless of what your app’s interface does. You can change your entire interface, perhaps transitioning from one view controller to another, while continuing to spin the networking activity indicator in the status bar. On the other hand, if you merely add a UIActivityIndicatorView to your interface, it is likely to be swept right off the screen when one chunk of user interface replaces another.

A possible solution is to add the UIActivityIndicatorView to the window. The window has the advantage that is not going anywhere; your entire user interface comes and goes, but the window doesn’t. The window, however, is also behind your entire user interace. In order to prevent the UIActivityIndicatorView from being hidden behind your user interface, you can bring it to the front by increasing the zPosition of its layer. This works because by default the zPosition of the layer of every view is 0, so a zPosition of 1 is normally sufficient to put a view in front of everything else.

Toggling User Interaction

Another iOS 13 deprecation is the app-level ability to turn off user interactions temporarily. I’m talking here about the UIApplication.shared members beginIgnoringInteractionEvents, isIgnoringInteractionEvents, and endIgnoringInteractionEvents.

Like the network activity indicator, we’re speaking of something that you used to be able to do by talking to the shared application, but now you’re expected to implement it yourself at the level of actual views. Indeed, user interaction and networking tend to go together; my code has a tendency to turn the network activity indicator on while turning user interaction off, and vice versa.

It’s a pity to see this feature go, because it was so delightfully powerful. The rule was that calls could be nested; there was a stack behind the scenes. So if your code said beginIgnoringInteractionEvents multiple times, possibly from different places in the code, user interaction wouldn’t return until you had said endIgnoringInteractionEvents that same number of times to balance them all out. isIgnoringInteractionEvents would tell you that you were still currently somewhere inside that nest, though it didn’t tell you how deep; in this way, you could avoid calling beginIgnoringInteractionEvents if you were already ignoring interaction events, or you could restore user interaction by calling endIgnoringInteractionEvents in a tight while loop until isIgnoringInteractionEvents was false.

If we are not aiming to emulate that sort of stack, simply turning off overall user interaction and turning it back on shouldn’t be that difficult. After all, isUserInteractionEnabled is a UIView property. And there is one UIView that embraces all others and therefore toggles user interaction everywhere — yes, you guessed it, it’s the window again! To turn off user interaction for your whole app, just turn it off for the window.

The Window

You may have noticed that I keep talking about “the window.” The network activity indicator is deprecated, so we propose to attach an activity indicator view to the window. Turning off user interaction at the level of the application is deprecated, so we propose to turn it off at the level of the window.

But how do we get a reference to the window in the first place? Before iOS 13, a very simple way was to ask the shared application for its key window: UIApplication.shared.keyWindow. Well, guess what? That’s deprecated too!

The reason has to do with scenes. Starting in iOS 13, an app can have multiple windows on iPad. Even if it doesn’t have multiple windows, and even if we are not on iPad, the app does nevertheless participate in this architecture. Every app running on iOS 13 has at least one scene (UIScene). Actually, what it has is at least one window scene (UIWindowScene, a UIScene subclass) — and each window scene holds a window. So we are not supposed to talk about the key window any longer, because there might be more than one.

Now, in actual fact, if your app doesn’t have multiple windows, there will be only one key window. And since the app still provides you with a list of its windows, you can find the key window by looking through them for the first window that is key:

UIApplication.shared.windows.first { $0.isKeyWindow }

I’m not sure, however, that that’s the best way. After all, you’ve probably got a view controller whose view is in the interface, so I think a more direct and reliable approach is just to ask for that view’s window. That, after all, is definitely the window we’re looking for.

In my apps there is always one view controller whose view is permanently in the window: the root view controller. (Some people believe that it’s okay to swap out the root view controller and replace it with another, but I don’t; instead, I always have a permanent root view controller, and if I need to replace the entire interface, I swap out the root view controller’s child view controller and replace it with another.) Knowing the class of the root view controller and its place in the app’s architecture, it should be a simple matter to access that view controller. The root view controller can then be responsible for showing and hiding the network activity indicator and for toggling user interaction off and on in its own view’s window.

The Status Bar

A number of read-only properties reporting things about the status bar are deprecated in iOS 13. Formerly, if you wanted to know the current state of the status bar, you would ask the shared application; now you’re supposed to ask the window scene’s status bar manager instead. This is an object of class UIStatusBarManager; it has properties statusBarFrame, isStatusBarHidden, and statusBarStyle (reported as .darkContent or .lightContent).

This works even if your app doesn’t explicitly support window scenes, because, as I’ve already said, there is always an implicit window scene. The easiest way to access the window scene is through the window, which, as I described in the previous section, is most directly accessed through a view controller’s view.

For example, assuming that self is a view controller, you could find out whether the status bar is hidden like this:

if let sbmanager = self.view.window?.windowScene?.statusBarManager {
    let isHidden = sbmanager.isStatusBarHidden
    // ...
}

Another shared application property, the statusBarOrientation, is deprecated as well; it is replaced by the window scene’s interfaceOrientation property. In the same way, all the notifications informing you that the status bar is changing its orientation or its frame are deprecated. Instead, you should implement the view controller’s viewWillTransitionToSize and examine the status bar (by way of the window scene’s status bar manager!), before and after the start of the transition.

The status bar orientation change notifications are important chiefly as a way of learning that the app’s interface is rotating. So how would we do that using viewWillTransitionToSize? Here’s an example of how to use the status bar manager’s interfaceOrientation to learn whether the view is changing size because it is rotating:

override func viewWillTransition(to size: CGSize, 
    with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to:size, with: coordinator)
        let before = self.view.window?.windowScene?.interfaceOrientation
        coordinator.animate(alongsideTransition: nil) { _ in
            let after = self.view.window?.windowScene?.interfaceOrientation
            if before != after {
                // yep, it's a rotation ...
            }
        }
}

Conclusions

The change in activity indicator view styles is just a mild inconvenience. The changes to the shared UIApplication are considerably more significant, and rather more puzzling. It isn’t entirely clear to me why Apple has imposed these deprecations on all apps, even those that don’t have multiple windows — indeed, even those that explicitly don’t even adopt the scene architecture. I suppose it’s just a matter of uniformity.

In any case, eventually Apple might decide to remove these deprecated methods and properties entirely, so it’s probably a good idea to start dropping their use now, before that happens. I hope this article has given you some ideas as to how you might do that.

You Might Also Like…

Picking a Photo in iOS 14

If your app puts up an interface where the user gets to choose a photo from the photos library, you’re probably familiar with UIImagePickerController. Indeed, you’re probably all too familiar with it. UIImagePickerController is a remarkably clunky, aged piece of interface, both from the user point of view and with regard to its programming API. …

Picking a Photo in iOS 14 Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.