Each year, like clockwork, for better or worse — sometimes, in my opinion, very much for worse — Apple releases a new version of iOS. We’re all used to the forced march of the calendar by now. In June, WWDC is held, and the new version appears in beta. In September, it goes final.
And so, each year, I have to take stock of my existing apps in the App Store and consider whether to update them.
It’s never an easy decision. For the most part, an app that was compiled against an earlier version of iOS will keep running in a newer version of iOS. So from one point of view, if you don’t have new features to add or bugs to fix, the simplest approach is to do nothing: users may update their devices, but your app will just keep chugging along.
On the other hand, if you do have new features to add or bugs to fix, you are compelled to open your app in the latest version of Xcode and compile it against the latest SDK because that’s the only way you can submit an update to the App Store. Moreover, Apple can make changes that break even your existing apps — even though that’s not supposed to happen — and now, once again, you are forced to update your app.
Sigh! So let’s say you build and run your old app using the new version of Xcode and the new version of iOS. Immediately, you find yourself in an unfamiliar world. Things might not look right. Things might not work right. And you still have more decisions to make. Are you going to change the deployment target for your app? How backward-compatible do you want to be? Recent versions of iOS introduce many cool new features, but taking advantage of those while maintaining backward-compatibility is not likely to be easy.
So each year, reluctantly, I look and see. Experimentally, I open my old app with the new version of Xcode. Experimentally, I update the deployment target to the latest version of iOS. Then I try to build and run, and I see what happens. If there have been deprecations or other availability changes, I hope to find out about them. If there have been changes in the way the app behaves, I hope to notice them. It’s an unsettling process, but that’s the price of living in a world that updates itself annually, sometimes in radical, far-reaching ways.
Now that I’ve done this dance for some of my older apps, it occurs to me that it might be useful to put together a little guide on what you can expect when you do the same thing. This article is that guide.
I’ve Got a Little List
My main goal here is to help you keep abreast of the changes and new features in iOS 14 that you should be aware of. You’d like to keep up with the latest information, no doubt, but when you’re busy getting real work done, you don’t always have time to keep track. And, to be honest, Apple doesn’t make it easy to discover changed APIs. They used to publish an official unified list of diffs in conjunction with each major version release, but that list no longer exists (though there are third-party substitutes, such as this one). When a new system version is still in beta, you can go to the documentation web page for a particular class and consult the API Changes display, but the window for doing that has now closed for iOS 14. You could examine the headers for every single class and consult the availability annotations, but who’s got that kind of time?
To help you, therefore, I’ve assembled this checklist of the main iOS 14 changes that I happen to be aware of. You can look over this list as a way of determining whether there’s a chance of your own code being affected. This list is Cocoa only; SwiftUI is a different matter entirely. I’ve concentrated on core Cocoa areas, such as UIKit, along with a few commonly used ancillary frameworks such as PhotoKit and MapKit; there are a lot of other frameworks that might be important to you, such as ARKit or CarPlay, but I haven’t even looked at those. Also, I don’t mention big new features that anyone would know about from watching the WWDC keynote video, such as app clips and widgets.
But I didn’t stop there. Well, I did stop there, initially; but then I changed my mind and decided to do more. For good measure, I’ve also included some of the most important changes from iOS 13! If you’re like me, you might have apps that haven’t been updated since Xcode 12 or earlier, so when you update to iOS 14, you’re also going to have iOS 13 to deal with. And iOS 13 was a big update; it introduced a lot of far-reaching changes. So it’s productive to think of iOS 13 and 14 together as a new world.
I’ll start by listing some of the main changes in iOS 13 and iOS 14 that might actually break your app.
Who can forget the collective trauma, the cry of sheer mystified agony, that rippled through the iOS programming world on Stack Overflow, as people ran their apps under Xcode 11 and iOS 13 for the first time? “My presented view controller looks funny! It doesn’t cover the whole screen! And when it goes away, I don’t get a
viewDidAppear call on my other view controller!”
Of course, one must wonder whether all these surprised people had been vacationing for several months on Jupiter’s moon Europa; how else could they have failed to hear about this change? Apple could not have called it out more strongly if they had released fireworks while standing on their heads on top of the Empire State Building.
Nevertheless, the fact remains that it’s a huge change. To sum up: In iOS 13, presented view controllers are no longer fullscreen by default, and if you were counting on
viewDidAppear to tell you when the presented view controller is dismissed, your app will break. Working out a strategy for coping is up to you; either set the presented view controller’s modal presentation style to
.fullScreen explicitly or adopt the proper UIAdaptivePresentationControllerDelegate methods for learning of dismissal.
Dark mode was introduced in iOS 13. The user can switch to dark mode at any time, so it is extremely important to test your app’s interface in dark mode. If your app was last compiled in iOS 12 or before, there’s a very good chance that when you compile it in iOS 13 or 14, entire areas of your interface will be unusable when the device is switched to dark mode because they are now dark text on a dark background. So turn on dark mode and examine every scene of your app to make sure that the interface is still legible.
As for what to do about dark mode, I can’t really tell you that. You can turn off responsiveness to dark mode in any part of your app’s view controller hierarchy by setting a view controller’s
overrideUserInterfaceStyle, but presumably it would be better to “do the right thing” by making your interface fully compliant with dark mode.
Bar appearance changes
On the surface, the change in iOS 13 bars that you’ll probably regard as breakage is that, by default, a navigation bar is now transparent when displaying a large title.
You can prevent this transparency if you don’t like it, but in order to do so, you will have to adopt the new classes for describing a bar’s appearance, such as UINavigationBarAppearance. If you’re going to do that, you will probably want to be consistent and use UIToolbarAppearance (for toolbars), UITabBarAppearance (for tab bars), and the entire family of classes that accompany them.
On the whole, these new classes work very well and do a better job of letting you dictate the look of a bar and its items than the old family of methods taking a bar state and bar metrics. Nothing forces you to use the new classes, but it is possible that you will have old appearance code that won’t work properly unless you adopt the new classes.
(However, I must warn you against the new tab bar appearance classes; in my experience, they are so buggy as to be just about unusable.)
Symbol images were introduced in iOS 13. In general, you can adopt these as desired; nothing compels you to use them. But if you did adopt them in iOS 13, your code can crash or fail to work correctly in iOS 14 because some of the symbol images are renamed in iOS 14.
Be aware also of the so-called multicolor symbol images introduced in iOS 14. These can give surprising results if you’re not conscious of them because they have inherent colors of their own and respond differently to tinting than normal symbol images.
Text field backgrounds
In iOS 14, UITextField background images are drawn in a new way. I’m talking here about the text field’s
background property. This isn’t commonly used, but if you are using it, the interplay between the background image and the text field’s own border may surprise you.
In iOS 13 and before, the background image was drawn in front of the border. But in iOS 14, once you set the
background, the border is removed. I suppose Apple is thinking here that as long as you are drawing the background, you might as well take charge of drawing the entire background, including the border. So that’s what you’ll have to do, if you were hoping to see a border.
Cell content views
If you were adding subviews directly to a cell (UITableViewCell or UICollectionViewCell), iOS 14 will represent a breaking change for you, particularly if those subviews were supposed to be user-interactive (such as a UIButton or a UISwitch). The reason is that the cell’s content view will now be in front of those subviews and can thus conceal them or prevent the user from being able to touch them.
The correct procedure is to add cell subviews only to the cell’s
contentView. And that was always the correct procedure! Adding direct subviews to a cell in any other way was always wrong, but there was never a penalty for misbehaving — until now.
User chooses a photo
If you’re using PhotoKit to access the user’s photo library, you can get a shock in iOS 14. Consider the following scenario:
You request authorization, and you get it.
You put up the UIImagePickerController.
The user chooses a photo.
In the delegate method, you retrieve the
infodictionary — and it’s
With the very same code, in iOS 13, the PHAsset was a real PHAsset; that’s why you requested authorization in the first place. So what’s going on?
The problem is that the user can now grant your app limited access authorization, effectively specifying which assets can be retrieved. Your old code cannot cope with this at all. (And, to be honest, I don’t think the runtime behavior is particularly coherent either; why is it telling you you’re authorized if you’re not?)
In iOS 14, there is a completely new API for working with limited authorization. Basically, you have to use a different authorization request method, check for a different authorization status (because it might be either
.limited), and put up a different image picker (PHPickerViewController). If you have configured the picker correctly, using the PHPhotoLibrary shared instance, you will now receive an asset identifier when the user chooses a photo. If you have only
.limited authorization, you still won’t be able to use that identifier for anything unless this is one of the limited authorized photos, but at least your code will behave sensibly.
New in iOS 14, the user can grant your app location authorization with reduced accuracy. If your code was expecting a location to be an actual location, that expectation can break the app. You need to check the horizontal accuracy of the location and not expect that it will ever be smaller than several miles or that its center is where the user really is.
Order of launch events
If you updated your app to iOS 13 or created it with Xcode 11, you may have adopted the new UIScene-based app architecture (I’ll talk more about that later). In that case, you will discover that in iOS 14, the order of events on launch has changed from iOS 13. In iOS 14, all the application and scene delegate events precede all the view controller events.
This is potentially a breaking change. It shouldn’t be, because you really shouldn’t be relying on anything about how those two sets of events are interwoven. But my experience is that one is strongly tempted, in the view controller, to expect that the application and scene delegate lifetime notifications will arrive at a definite point in the view controller’s own lifetime.
In iOS 13, the
sceneDidBecomeActive(_:) scene delegate event, and therefore the UIScene
didActivateNotification, first arrives between the view controller’s initial
viewWillAppear(_:) and its
viewDidAppear(_:), so you may have taken that ordering into account in deciding when to register for the notification. But in iOS 14, the scene activates on launch before
The UISegmentedControl was revised in iOS 13. For the most part, this probably won’t matter very much to you; the segmented control is drawn differently, but it works much as it did before. However, there is one huge change that can surprise you: starting in iOS 13, the
tintColor, whether applied directly or inherited from higher up the view hierarchy, has no effect on the tint of a segment’s title or image. You’ll know this has happened to you when your segmented control suddenly appears to have no tint color. (It does actually have a tint color, but it’s the
.label dynamic color, introduced in iOS 13 as part of dark mode.)
The solution, if you want to dictate the color of your segment titles and images, is to set the
foregroundColor of the
titleTextAttributes. I regard this as a bug — what on earth do the title text attributes have to do with the color of an image? — but, to my amazement, Apple has told me outright that this “works as intended.”
Bar button item background images
In iOS 12 and before, if you had a UIBarButtonItem with a background image that wasn’t the same size as the bar button item, it would be resized automatically to fit the bar button item. In iOS 13, Apple did away with this automatic resizing. Depending on the exact dimensions of the original image, this could cause the bar button item to appear in the wrong place or to display the image incorrectly.
For me, this was a very severe breaking change in iOS 13 because it affected even my existing apps that had not been recompiled. The workaround is not difficult — just resize the image in code to within a standard 24×24 points or so. But to do that, I was forced to recompile the apps under iOS 13, thus subjecting them to the risk of all the other breaking changes I’ve talked about. You can run, but you can’t hide.
Back indicator image
This one is tiny, but it managed to bite me anyway.
In iOS 13, a UINavigationBarAppearance method
setBackIndicatorImage(_:transitionMaskImage:) was introduced. It takes two images: the image to replace the chevron at the left of the navigation bar and the mask on that image. But there was a bug: Apple got the two images backward! So, naturally, I swapped the images so that things would work properly.
But of course, by the time iOS 14 came along, Apple had fixed the bug, so I had to swap the images back again.
Other Big iOS 13 Innovations
As I mentioned before, iOS 13 was a big release. It contained too many changes and innovations to list here, but let me just briefly review the main ones that you’ll want to keep in mind if you’re grappling with iOS 13 for the first time.
In iOS 13, iPad apps can have multiple windows. To facilitate this change, all apps, whether iPhone or iPad, whether supporting multiple windows or not, have their interface wrapped in a UIScene.
UIScene and the accompanying scene architecture are a major innovation in iOS 13. You don’t have to adopt the scene architecture explicitly (though even if you don’t, there will still be a UIScene). But if you do adopt it — which is signaled by the presence of the Application Scene Manifest entry in the Info.plist, or the use of UIScene app delegate methods, or both — you now have a scene delegate, and a lot of events that used to arrive at the app delegate will now arrive in the scene delegate instead. Your code may have a lot of adjusting to do.
A common mistake is what I might call partial adoption of the scene architecture. The usual sign of an issue is that the app launches into a black screen. This could be caused by something as simple as assuming that the app delegate still holds the
window property. It doesn’t; the
window now belongs to the scene delegate.
There are a whole bunch of deprecations that go along with this change; you should be able to cope with them without my advice. However, there’s one deprecation from which I have never recovered, and for which I have no solution: Apple announced at WWDC 2019 that the entire view controller–based state saving and restoration mechanism (UIViewController
restorationIdentifier and so forth) should no longer be used — and, true to form, they did not explain in any detail what on earth is supposed to replace it.
Table view and collection views
My favorite iOS 13 innovations are diffable data sources and collection view compositional layouts.
A diffable data source can do almost everything that an old-fashioned data source can do, plus it can store the data for you, plus it takes care of animating changes in the data (such as insertion or deletion of a row) in an automatic, coherent way. Diffable data sources aren’t perfect, and you’re not required to use them, but they are better in iOS 14 than they were in iOS 13, and you might want to consider adopting them.
Compositional collection view layouts are much more powerful and flexible than the flow layout, which was previously the only built-in layout type. The result is that you might be able to achieve the desired look and behavior without writing your own layout subclass or hacking at the flow layout.
In iOS 13, 3D touch is basically dead, being replaced by a long press that works on any kind of device. This means that “peek and pop” of view controllers, in particular, is deprecated; it is replaced by a new context menu mechanism with classes like UIContextMenuInteraction, UIContextMenuConfiguration, UIMenu, UIAction, and so on.
Aside from the difficulties of backward compatibility, this is an easy and clean change to adopt, and you will probably want to do so.
The Combine framework, introduced in iOS 13, is really great for communication between objects, asynchronous activity such as networking, multithreading, and a host of other things. If you haven’t been using a “reactive” framework, you’ll have to think in a whole new way. But I can safely say that my code is far cleaner everywhere that I’ve adopted Combine, and I strongly recommend taking the time to study it and adopt it wherever possible.
New in iOS 14
Here’s a checklist of additional changes in iOS 14 that you might like to be aware of. I’ll start with some major changes; then I’ll just list a whole bunch of lesser changes for the sake of completeness.
UITableViewCell now has
backgroundConfiguration properties, along with an
updateConfiguration(using:) method. The idea, architecturally, is that your
cellForRowAt data source method should no longer configure the cell interface directly (if that’s what it was doing); instead, you pass the data through a configuration object to the content view of the cell, and let the content view, or the cell itself, be responsible for worrying about the interface.
There are similar changes for UITableViewHeaderFooterView and UICollectionViewCell; and the
imageView cell properties are slated for deprecation. In their place, you are expected to use UIListContentConfiguration or a custom content configuration type.
You’re not compelled to adopt any of this, but it can improve your code by separating responsibilities appropriately, as well as by giving your interface better consistency of appearance (because a content configuration object can be used in a table view, a collection view, or anywhere in your interface).
Collection view innovations
There are a lot of improvements to collection views in iOS 14:
CellRegistration and SupplementaryRegistration objects
Diffable data source section snapshots
Collection view lists
Hierarchical lists (outlines)
Also, diffable data source response to user rearrangement of cells is now automatic (and the diffable data source has a
Split view controller overhaul
The UISplitViewController architecture has been completely overhauled in iOS 14. Having rewritten one of my older apps to adopt this new architecture, I’m here to tell you that this new architecture is terrific. Instead of having to jump through incomprehensible hoops just to get standard split view controller behavior, everything is done for you automatically. Vast chunks of my old code went away completely!
The hard part is grappling with cases of “live” collapsing or expansion of the split view controller, as when a “big” iPhone is rotated or when an iPad app is engaged in iPad multitasking (splitscreen or slideover). However, it’s not that hard; my app seems to be working fine, and the code is a lot easier to understand than what I had before.
A UIControl can now have an embedded UIAction instead of being assigned a target–action pair. This can make for much cleaner code; you can do away altogether with the separate control event handler method. (It’s a pity that Apple didn’t do the same thing for gesture recognizers.)
Also, a control can have a menu that pops up when long pressed (or, optionally, when tapped). You are expected to use button menus rather than action sheets in most situations.
There is quite a bit of new API for this, especially for UISegmentedControl and UIButton (and UIBarButtonItem, because it is button-like).
This is a list of other iOS 14 changes that you might want to be aware of, in no particular order (well, there’s an order, but I’m not going to explain what it is). Some of them are very minor, but I’m trying to be as complete as I can:
.fixedSpacesystem bar button items are superseded by class methods.
A UIMenu can have a menu item that is a UIDeferredMenuElement. This means you don’t have to supply menu items (UIAction or UIMenu) until the user actually displays the menu.
Various new classes and methods take account of the possibility that the user will connect an external pointing device or keyboard to an iPad (or that an iOS app will run on a Mac). This includes the expansion of gesture recognizer capabilities.
NSAttributedString now has a
.trackingkey. (This has actually been present in Core Text since iOS 10. Who knew?)
pageZoomproperties, and so on.
The SFSafariViewController delegate tells you when the user reopens the page in Safari.
There are some significant basic interface changes. The UIDatePicker interface is completely revised. A UIPageControl now allows selection by sliding (continuous interaction) and is horizontally scrollable; you can also customize the “dots.” The color picker view controller and color well control are completely new.
Local notifications have ephemeral authorization (for app clips), and UNNotificationPresentationOptions
.alertis replaced by
Stereo sound recording is now available on some devices. Lucky you!
prefersAssistiveTechnologySettingshonors user voice settings such as speech rate.
Picture-in-picture video playback now works on iPhone (previously, it was confined to iPad).
As I mentioned earlier, UIImagePickerController is replaced by PHPickerViewController for user selection of assets from the photo library, and you might need to use it in order to cope with the possibility of limited access. There’s additional new API; for instance, you can put up a limited library picker, asking the user to grant access to additional assets. To learn whether the user has changed the list of accessible assets, use the PHPhotoLibraryChangeObserver. Also, you can now explicitly request add-only authorization (previously, this was requested implicitly when you tried to add something).
In the Map Kit world, annotation view collision mode can be
.none, exempting the view from collisions, and layering order can be dictated with
zPriority. MKPolylineRenderer has a new subclass MKGradientPolylineRenderer. An MKLocalSearch can consist of just points of interest. The MKMapItem
openInMapsmethod changed to acquire a UIScene parameter and a completion handler (actually, that started in iOS 13.2).
authorizationStatusis now an instance property (previously, it was a class method). The delegate method
locationManager(_:didChangeAuthorization:). When you call
requestAlwaysAuthorizationwhen you already have When In Use authorization, an authorization alert now appears (actually, that started in iOS 13.4).
UIDocumentPickerViewController has new initializers, replacing the UIDocumentPickerMode specifying what you want done with the chosen item.
A universal link’s
applinks:entry can specify developer mode so that you can test without caching.
The UTType struct (
import UniformTypeIdentifiers) replaces the CFString UTI constants starting with
kUTType, which are deprecated.