Ellen
I hope you all got plenty of caffeine during the break, because I'm about to throw a whole lot of information at you. So every year, we as Apple developers collectively experience the Worldwide Developers Conference. Absolute mountains of information dumped on us, about 50 videos at a time over the course of five days.
And during WWDC, you get incredibly excited about all the cool new stuff that got announced and that you're really itching to use. But then you go back to your job, the thing that actually pays you the money you need to live. And for folks at most jobs, that means that learning that you're not actually going to be able to use any of this stuff that just got announced for the next two years.
Because your app has to support at least two versions back from current. And you will definitely have some arguments with your product team about why you can't just drop support for the old and busted operating system so you can just use all of this new hotness. And they'll point to things like how many of your existing customers are using the old version of the OS.
And when you point out that your existing customers who already have the app installed can keep using the prior version, that still might not be enough. Because even if your number of existing customers using something is pretty small, if your number of new customers using older iOS versions is high enough, that means that you essentially will be showing people a dialogue that says, sorry, not sorry, you can't install this app, we don't want your money. I am told by a friend who has an MBA that this is not good for business.
There are some moderately insane workarounds to this, like telling the user to install the app on a newer device, then pull up your app in their app store history, and then they'll have the old version get offered to them. Your average customer is probably not going to want to deal with all of that for an app that they're not even sure if they want to use, let alone pay for. So you decide, okay, we will come back to this the next time a new version of iOS comes out.
And over the next couple of years, time moves along, and you wind up forgetting a ton about what was actually released by the time you actually get to drop to using that year's new hotness as your minimum OS. Some of us forget more than others. For instance, my memory is such hot garbage that I completely forgot until I got about halfway through preparing this talk that I did an entire talk about WWDC 22 in 2022 when I tried to speedrun watching as many videos as possible in three weeks and failed somewhat spectacularly because the number of videos is truly absurd.
So if you're like me, working on an app whose minimum target is stuck sitting somewhere in the iOS 15 range, but have utterly forgotten what happened just two years ago, let us work together to refresh our collective memories. Let us step not into the hot-dub-time machine but into the older, whiter Apple version of this, the dub-dub-time machine. And here we will learn about the glorious old toys, old new toys that are available to us when we update to the brave new world of iOS 16.
There was actually quite a lot in the realm of SwiftUI that was introduced that year. It had been three years since it was initially introduced, and there had been a lot of pieces that people weren't really happy with the original implementation of. Apple decided, let's revisit some of these designs.
The biggest one is that they decided to take the functionality of the existing navigation view, which handled both normal navigation layouts and split view layouts, and chop it in half. So now we have separate view types for the simpler version and the more complicated version, which means that if all you're handling is the simpler version, you don't have to jump through a lot of hoops to get what you want, and in both of these, there's way, way better programmatic navigation for things like deep links, something that if you've ever tried to do that with just a navigation view, you will be very, very thankful for.
I'm going to talk through this using an example with a navigation stack, mostly because it makes how it all works way simpler to explain. So one really nice thing about these changes is that you can move the handling of your navigation outside of your local view. So in this case, as long as you've got a published array of some type conforming to hashable, you can use that to handle setting up your navigation stack, which takes a binding of a navigation path, which is secretly just an array under the hood.
Once you bind it, you can tell the next view how to handle any changes to that array by saying, hey, when the navigation's destination type is the same as this array's type, here's how you handle it, and you can still trigger this by having the user tap on a navigation link that will push the next value onto the navigation stack, and that looks pretty similar to what would have looked like before. Pushing and popping as you tap navigation, and then you push the back button.
It's also possible to navigate programmatically, either from a button press or from something you catch somewhere that has access to whatever is hanging onto your path array. By both updating the array that you have representing the path to include multiple items, and then handling all of the items in the navigation destination modifier, handling your type, your app will know that it should push all the items onto the stack at the same time, and then when you hit the back button, it will be as if you had gone individually to each of the things added to the stack. Another nice thing is that because you can pass that navigation model around, you can pass it down to the child views and have it do stuff like completely reset the navigation path so that when the user does something like pressing a logout button, the user gets taken straight back to your root view, and this is a simple version that works great for most iOS apps.
If you're going to be supporting iPad OS, you're going to want to read up on navigation split view. On an iPhone, this will essentially behave like a navigation stack, but on iPad, you can have it be up to three panes to allow people to drill down super easily before they get to a detail view. It also does all sorts of nice UI split view things like handling swipe gestures and automatically collapsing to show just the detail view.
The next thing Apple added to address some issues folks had was a type called any layout. This solved a pretty common problem that folks ran into when they were trying to create a single view with one layout when the horizontal size class was compact and one when it wasn't, usually corresponding to the device being rotated into landscape, but potentially also with stage manager on iPad. If you tried to do it this way, with a closure returning either an HStack or a VStack as applicable, you'd get the dreaded warning that your underlying types were not matching, and everyone knows that the easiest way to solve this is to just slap some TypeScript erasing any views on there, and voila, everything is now the same type and we can move on with our lives, right?
The only problem is that with any view, you lose a ton of benefits of Swift UI in terms of the automatic recomputing of only the portion of the view that changed because the type erasure performed basically disables all of that, so your whole view will wind up getting redrawn way more often than you want it to, and that's why Apple came up with any layout, which is a way to keep that highly performant Swift UI going by returning something that's always the same type, since it will always be wrapping something that conforms to a protocol called layout, and this means that you're going to see some changes in your view setup code. First, you're going to notice that you're no longer wrapping up HStack and VStack, they're HStack layout and VStack layout, they're new types that promise to do the same thing as HStack and VStack, but much more flexibly because they conform to the layout protocol.
These new types also no longer take trailing closures within the body of the stack, so they have to be set as a single variable, which then takes the trailing closure of the rest of the view you want to wrap up. These are really useful not just for adjusting the orientation of your size class, but also for adjusting to dynamic type size. After all, something that might be really easily readable as two texts in an HStack at a smaller size might just need to be a VStack at accessibility sizes.
And Apple has included layout support for ZStacks and grids as well, so you don't have to write your own layouts for those. But if you really want to write your own layout, well, have fun. In theory, you could probably do something that's not that dissimilar from a collection view layout, since you do get a fair amount of control over where views are placed.
I generally haven't seen a ton of people doing that. Another thing that Apple made way, way easier to deal with that I will mention briefly is using SwiftUI for cells within UI kit classes like UI table view and UI collection view. And they're doing this with a class called a UI hosting configuration, which takes care of everything you used to have to do in terms of creating a UI hosting view controller, getting its view embedded in the cell.
Now you can set up a content configuration and either construct your SwiftUI view directly in place or pass an existing SwiftUI view into that hosting configuration. By default, hosting configuration will handle taps on the full item for you through UI kit delegate methods. You can still add buttons within the view in SwiftUI that will intercept the tap in that part of the view.
There are also some APIs specifically named are swipe actions and separator alignment that get bridged between SwiftUI and UI kit automatically. You do need to look out, because not everything is bridged, and I was not able to find a canonical list of what is and what isn't. So in addition to these improvements to an existing UI, there are some brand new types in SwiftUI that will let you display some fun things that you used to have to build yourself.
The biggest one of these is SwiftCharts. It gives you the opportunity to define data and then figure out how to display it in a chart and then potentially makes changes to the data or the display of the chart really quickly. So to show you all an example, I decided I wanted to graph how much ham I eat per week when I am in three different countries, the U.S., Spain, and France. So I made a nice little struct to hold that information for the sake of convenience. I defined some static variables for the titles to make a couple of things easier later, and then I started with a bar chart, which is probably the best way to describe this kind of data. First, you set up a normal declaration of a view, pass in the data you want to display.
Then you declare a chart, pass in the key path of the data you want to display, and you can set up labels for the X and Y axes or basically any other information you want to set up. Then you take your data and you turn it into the type of view you want in order to display your data. The bar mark shown here with an automatic width will figure out how much space it has and then display your data at a width that makes sense for how much data you're trying to display.
You can also specify a width if you think the DeWalt bars are too fat or too skinny, although you should be aware that if you specify a width that's larger than what the automatic setting would give you, SwiftUI will assume that you want it to overflow rather than correcting that, so be careful about that. Switching the code you've written from a bar chart to a line chart means that you wind up with something incredibly similar. The only difference in this code is that you change the bar mark to a line mark.
Et voila, you have the same data displayed as a line chart, and it becomes really ridiculously easy to make something that allows you to switch between the different views of the same data in a way that's very performant. And all of these different pieces of the Swift charts have tons and tons of options you can use to modify the default appearance, but, again, if you decide you want to make something stupidly huge, SwiftUI will assume you meant to do that, even if it means overflowing the bounds of the chart and covering up part of your access marks and labels, so watch out for that. If you want a great place to start with Swift charts, Uri Brown has a fantastic open source set of examples up at GitHub that I really recommend checking out.
They show really the full breadth of what's possible with Swift charts, and it is a lot. Now, Swift charts does have one major limitation you need to be aware of, and I was mystified by it because everyone loves pie and pie charts. Apple did not include any polar geometry in the initial version of Swift charts, so there were no pie charts or other radio charts like donut charts in the initial version of Swift charts.
They did add them for iOS 17. Since this talk is about N-2, I get to complain about this and or warn you about it. Next up in the new choice category is an official way to make these nice sheets that slide up from the bottom of your screen and can be expanded and collapsed by the user.
Here I want to thank Paul Hudson for putting a great example of this together, which I have frequently stolen for this demo. Here you can see that when you press the button, you toggle a variable that goes into the sheet API, allows you to show a very, very simple sheet that slides up from the bottom of the screen. And if you want to be able to work with it at multiple sizes, you can use something called presentation detents to declare what positions you want the user to be able to resize the bottom sheet to use.
You can hand it custom detents that specify either a hard-coded size or a percentage of the screen that you want covered, or you can use some nice built-in convenience detents to make it really easy to have the system give you some pretty reasonable spots within your screen to have the sheet land. And if your designer looks at this and goes, oh, my God, why does that hideous gray thing in the middle of the view now? Good news.
You can just add a line to hide the presentation drag indicator and you'll be back to your nice crisp design, but with the same functionality to be able to drag it up and down between the areas you told it to go and then also give it a nice hard swipe to get rid of it completely. Another thing that people were getting awfully tired of writing themselves was a date picker that allowed you to pick multiple dates. The basics of it are pretty simple to set up.
You can just set up your content view with a set of date components objects to bind to the multi-date picker, then fire it right up with that binding, and you get a very nice-looking calendar that has conveniently already highlighted today's date for you. And when you tap on the name of the month, you automatically get a picker that lets you choose a different month and year, and then your user can go ahead and pick a whole bunch of days and take those date components and figure out what to do with them. Now, one thing to be aware of and that differs from quite a number of third-party libraries on this is that by default there isn't a simple way to let a user pick a start date and an end date and then have any dates in the middle fill in.
You'll basically need to watch the selections, then do a bunch of hard-to-get-right calendar math to figure out when there's an empty day in between and then update the binding. This is definitely better than not having it at all, but it's also definitely not a complete solution, particularly if you're doing something like a reservation app where a non-contiguous set of dates makes absolutely no sense. So now that we've seen a whole bunch of stuff that was added to SwiftUI, I want to talk a bunch of stuff that was still UI-related, but not specifically tied to the SwiftUI rendering system.
The biggest one to my mind was TextKit 2, which Apple had previewed a bit on Mac OS the previous year, but wasn't really ready for public use until iOS 16 and its various friends came out. Here I must give a small disclaimer that TextKit 2 has been the bane of my existence for almost a year because I decided to rewrite a fairly complicated untested text engine for the journaling app I work on without writing the tests in place first, which was a seriously terrible idea. Thank all the gods it's almost done.
But I'm going to try to be fair here because my experience is not really representative of what most people will run into. I honestly dug my own grave with a lot of it, and there are some truly useful things in here. The most noticeable changes are that you will find the word text shoved into all kinds of class and property names to indicate that you're working with TextKit 2.
One thing to really watch out for is this property name change on UITextView. This is how you wind up indicating whether you want to use TextKit 1 or TextKit 2. If you ever attach things to the dot layout manager, you're going to be rendering and getting callbacks in TextKit 1, even on newer operating systems.
If you only ever use the dot text layout manager property, you will be on TextKit 2. You also get some nice new delegate methods like the text content storage delegate, which lets you do some fun manipulation to your text as it comes out of storage before it's handed off to the text layout manager delegate, which allows you to use a stock text layout fragment or a custom one of your choosing to facilitate using custom attributes. There are some pretty significant advantages to using TextKit 2.
The first is that you get far better separation of display and storage concerns, and the ability to pull unstyled text out of storage and style it on the fly. The example app they show is storing comments with just a single attribute indicating that, yes, this is a comment, but displaying it with a different background colour and text than whatever it's replying to. This makes it much easier to write out to storage.
Only what you need to know to construct your UI and then actually apply the UI changes later. A good way to do this is to use custom subclasses of NSLayoutFragments which allow you to write a subclass for display of a certain type of text. In the example app, they use posts versus comments to show what's possible in terms of drawing things more indented based on the depth of the comment.
The other piece that's super helpful is that handling languages where characters use multiple glyphs for a single visible character. Example would be Chinese and Japanese, also emoji. It's much, much easier than trying to identify exactly which glyph indicated the start and end of what the user sees as a single character manually.
And there are definitely some things that are not great. Some of these are tied straight to TextKit 2. Others are tied to the general ecosystem in iOS.
I found it particularly annoying that there are UI text review bugs that still haven't been fixed. The most ironic one for me was that when adding a custom attribute to the typing attributes dictionary, it sometimes just gets thrown out of the dictionary for absolutely no reason. And given that custom attributes are how Apple recommends you annotate the bits of your text that are supposed to be displayed differently, I found it really obnoxious that you have to do some real bend over backwards work around to make that work as the user is typing.
So the other problem is that there are four different kinds of range you're going to wind up running into with all of this, and it's absolutely maddening. So UI TextView uses UI Text Range, which allows selections to be made in relation to the start and end of the document in the TextView. This is what powers the better emoji handling, because you don't have to actively figure out if your selection is around an emoji or compound character.
System does it for you. Then when you get down to NSTextRange, which is very similar but is used by all of the pieces of the TextKit 2 layout system. When you have an NSTextRange, you're not entirely ready to work with the underlying text, because almost all the NSString and especially NSAttributedString APIs that you're going to be hitting will need an NSRange, not an NSTextRange.
And then there are the occasions where somehow you wind up with a SwiftString or SwiftAttributedString which use SwiftRange rather than NSRange. And you can put methods together, jump back and forth between these types, but trying to remember what context you're in versus what context you need to be in is a giant pain. For example, something as straightforward as making the selected range in a UI TextView bold if you're using your own UI requires a really ridiculous number of steps.
First, you have to make sure everything is set up correctly so you're only dealing with one instance of your text storage, which is tied to your text content storage. Then you have to get the UITextRange of the selected text from the UITextView. And then you calculate the UITextRange's offset from the document beginning to the start location of the range to get the NSRange's location, then the offset from the start location to the NSRangeLid.
And then you use the NSRange to update the underlying content storage, attribute a string from a different font attribute. And finally, you have to wait for everything to reload, which of course reloads through a system that hits NSTextRange, because why would systems that we already have access to be useful there? It's absolutely maddening.
So you're probably wondering if there's any point at which you should even bother with the insanity of using TextKit 2, and the answer is, yeah, you should. The most obvious place is when you are building a brand-new app. Compatibility is pretty easy when you don't have anything to make it compatible with.
You can also use it when you have apps that have fairly straightforward text logic, like bolding the first line of text in a text view and then not really doing anything else. You get much clearer information about what the user has selected in a way that corresponds with what's on the screen, rather than how many glyphs are involved. You can also get away with using TextKit 2 if you have really well-tested text logic.
I cannot stress enough how much you want to have all the tests already in place to validate all the behavior you support before you start messing around with this conversion, because otherwise it's a giant nightmare. The other thing is that if you want to support Apple Intelligence stuff fully, you want to upgrade your text engine to TextKit 2. There are some things that will work to a certain extent with TextKit 1, but the highest quality interactions are going to come from TextKit 2.
If you have a complicated TextKit 1 app, you may start to feel like Apple is trolling you with this requirement, but if you're not planning on supporting Apple Intelligence for whatever reason, and you have some fairly complex display or storage logic, and you don't have that stuff tested to within an inch of its life, you may not want to put yourself through the trouble of trying to translate it to TextKit 2 until you've already written those tests.
Next, something that Apple decided to make easier to do a basic implementation of some very cool stuff is the data scanner view controller, which lets you scan QR codes and bar codes easily as well as showing a live view of text with detected data like phone numbers and URLs, and this is a massive improvement over what you had to do before to get any of that stuff. You had to build your own pipeline with either AV foundation or VisionKit depending on whether you were scanning text or bar codes, and then you had to translate between multiple coordinate systems to get UI to show up where you wanted it to in relation to the scanned objects. Now all you have to do is fire up an instance of data scanner view controller and you'll get a lot of what you already want.
You'll get information about the location of anything detected returned already translated into your view coordinate system. Apple is doing the math so you don't have to, so that's very nice of them. You also get unique identifiers for each item that was scanned so you can track things over time.
If you're using a delegate method, this makes returning UI for specific pieces of information really, really easy. Also in delegate methods, you get items returned in an array that's sorted by what Apple refers to as reading order, the logical order in which the user would read things visually, which you can use to facilitate some fun UI highlight animation. If you don't need UI, the good news is you can just skip the delegate all together, get a fancy async stream of all the items that are detected as you receive them.
So I've talked a ton about UI, and I want to highlight a few features that aren't entirely about UI. I'm going to start with a transferable protocol, which is actually sort of for UI and then it makes a bunch of UI-related things easier, but it doesn't include any UI itself, so I'm going to allow it. What the transferable protocol does is make it a lot easier to handle drag and drop and copy and paste across applications, and there are a bunch of base types that Apple provides you can use here, the proxy representation, it's for standard library types like string that have built-in conformance that any application should be able to decode.
Codable representation is great for small data structures that conform to codable, don't have any major data transfer requirements. Anyone who wants to receive this representation and any of the other couple that I'm going to talk about needs to opt in to whatever type you declare. And a data representation allows you to send raw data that's already in memory, usually something like an image.
You also can use a representation of a fairly large file written to disk like a movie and give a temporary sandbox extension to the receiver to retrieve it without having to load the whole thing into memory, just to immediately get rid of it. You can just return multiple of these representations if you want, but the order in which the order matters quite a bit, since the recipient will get whatever the first version they registered for is. If you return a codable type, then a proxy type, if the receiver can receive your custom uniform type, they'll receive that codable transferable.
If they don't, they'll just get the proxy with whatever fallback you provide, whereas if you do the reverse and pass the proxy representation first, the only thing anyone will get is the proxy representation, since everything can handle types that have conformance already added by Apple, and it won't even get to looking at your custom type. Next, let's talk about the fact that you can actually use Swift regex with iOS 16. Yay!
This was added to Swift 5.7 directly, but when you tried to use it with iOS 15 or lower, you ended up getting an error that basically said, sorry, we didn't implement some of these features as back deployable, so this needs to be used in 16 and up only. And if you've never worked with regex, a regular expression is a great way to work with a bunch of raw text that has a similar format, and you want to extract data from it in a way that's easier to work with. So this is some sample text about my current travels that I want to parse so I can get the method of travel and the destination.
And before Swift regex became available, you had to use an NS regular expression which basically took a regex string that would match the very complex annotations of regex that works in many other languages, then if the regex was valid, you could enumerate whatever matches it found. And this is a very, very simple regular expression, but it's still a little hard to read. So the first improvement that was added was support for regex literals.
These allow you to create a string that's at least marginally easier to read and access information about. In this case, the regex literal uses forward slashes instead of quotes to indicate where it begins and ends, and then uses this weird parenthetical called a capture group to indicate where you sort of want to have it fill in the blanks. Then you run that regular expression against your big pile of text, and you get a list of the matches to your query.
And here you can access pieces of the query by grabbing parameters of the output. You'll notice that you're grabbing the 0.1 and 0.2 parameters of the output. If you look at the type signature of this output, you'll see that there are three substring generic parameters.
0 is the full match of the regular expression, 1 is the first capture group, 2 is the second capture group. When you run this in a playground, you get a nice list of all the methods of travel and destinations printed out. So that's pretty neat, but it could be a lot easier to figure out just exactly what this code is doing.
And this is where my favorite bit, the regex builder, comes in. The regular expression builder basically takes regular expressions and turns them into a fairly verbose but far more comprehensible DSL. And a nice thing is that you've got a regex literal that you want to convert to something more readable.
If you hover over it in Xcode, you'll get an option to convert it to using the regex builder syntax. When you select that option, you'll suddenly have way more code. But at least in my opinion, this is a hell of a lot easier to read to figure out what it's doing.
After you declare that you're making a regular expression, you can throw in some string literals to represent things that need to match exactly. And you can far more clearly indicate that you're using capture groups and specify what they're capturing in a more readable fashion. And after making this change, your output doesn't change a bit.
You are absolutely not giving yourself any performance benefits here, but you are making it more readable. And you can also work to make it clearer and more type safe by creating references to the things you're capturing. So in this case, both of the things that I'm capturing are substrings of the original string, but you can also do things like capture integers or floats so you don't have to manually convert them from the strings after you get them.
These nice named references get passed directly into each capture group so that someone reading this later can more easily know exactly what's being captured by a given group. In addition, pulling data out of the indexes of the return tuple, you can change that to actually using the named references to subscript the match and get access to the same information. And again, the result here is not what changes.
The difference is in making sure that the next time you come back to look at your regular expression code, you are much more able to figure out what on earth it was doing. Next, clearing the extremely low bar of something that is more fun than regular expressions, we have weather kit. This introduced a way that you could access Apple's information about weather either through your app or a JavaScript API.
I'm not going to talk about the JS stuff because JavaScript is to say the very least not my area of expertise. However, when you work with the API, you need an API key to use it. With iOS, you set that up by using a specific signing entitlement, and you know you're about to have some serious fun when you start to read the documentation around a sample app, and it includes the phrase wait 30 minutes while the service registers your app's bundle ID.
I assume they're doing some edge caching with an API key validity if they're making it quite that explicit, but I don't know. Once you get that up and running, you can pretty quickly get information about the current weather at any location, switch up some parameters and types and get things like an hour by hour forecast or a daily forecast. There are other types available like active alerts and historical comparisons as well.
These queries look simple, but one thing to be well aware of is that the API key you've got is going to be helping Apple figure out how much data you're hoovering up request by request, and while there is a pretty generous free tier giving you 500,000 calls a month, pricing can go up pretty steeply from there, so be careful how frequently you're updating with this API. Also, since these prices are denominated in America bucks, as of today, the exchange rate means you basically get a 10 per cent discount for being in the Eurozone. Now, obviously, this is not a comprehensive overview of everything available as of iOS 16, but there were a few other things I wanted to at least mention because I think they will be fairly useful.
The most fun one is that you can now add multiple colours to different parts of an SF symbol with variable colour symbols. There's some fun new toys in MapKit, including server API support if you need to do something across platforms. There's some very nice quality of life improvements like the new API to handle automatic cure board and the ability to set a minimum and maximum number of lines you want to allow before starting to scroll text in a text field.
If you pass in an axis, you can continue to expand the text field to fit the text until it hits the max. There's also very nice convenience API which was added to make filtering media types like videos, live photos, slow-mo videos available at the point where you're selecting them. There's also initial implementation of passkey support so you can adapt to that technology if you want.
Finally, there's one thing that was introduced a bit late. In iOS 16.1, we got activity kit and live activities. The adoption of these was pretty limited at first, partly because the main use case that they supported was score changes in sports ball games.
But there's way more on this in the WWDC 2023 sessions and support for more types. If you really want to use this, you can try to look and see if you can crowbar your use case into the earliest APIs. With that, at last, we have arrived at our obligatory summary slide where I try to recap some of what I just threw at you for the last half hour.
iOS 16 has tons of features to make working with SwiftUI easier, especially around navigation and layouts which change based on things you don't control like orientation or text size. There's an awesome charting library, but until you get to a minimum version of iOS 17, you cannot use its implementation of Pi or other radial charts. As a reminder, TextKit 2 has fun new toys.
Only use it if you've thoroughly tested your text logic. There are a bunch of new UI ties that mean you can make Apple do a whole lot of the work that you used to do, and there are some data-only toys that you can use to get better data, transfer data more transparently, or make a notoriously hard-to-read type way more readable. And while I've thrown a ton of information at you today, this is most definitely not a comprehensive list of everything that is new in iOS 16.
I really recommend you take the time to poke around the videos from WW2022 because there is a lot of fun stuff buried in there. And with that, I thank you all for your time today.