Whether you're building apps for iOS, macOS, watchOS, or tvOS, a lot of the functionality you get from Apple's SDKs comes from the Foundation framework. Join the Foundation engineering team to hear about the improvements they've been working on. Learn how significant changes to key paths can help make your code safer with strong type checking in Swift. Hear all about how the new archiving API can help you safely convert your native Swift types to and from external formats like JSON. Gain insights into performance enhancements that will help make your app even more efficient.
Thanks for coming. My name is Tony Parker. I'm the manager of the Foundation Team at Apple, and I'm here today with my colleagues Michael LeHew and Itai Ferber to tell you What's New in Foundation.
We have three topics today. The first, we're going to go over really quickly some new API highlights from this year's release of Foundation.
After that, we're going to go into the first of our two major topics, the key paths and key value observation APIs.
And after that, we'll talk about our new encoding and decoding APIs.
So let's get started with those new API highlights.
First, this year, we've enhanced the FileProvider API that we introduced last year as part of iOS 10.
This enhances your ability to communicate between FileProvider extensions and other applications. We've improved our available storage space API.
This is new API on MSURL that lets you not only get an idea of how much free space is available on your customer's device but also how much space can be made available if we purge unnecessary content like caches or old data.
We've added new API to NS range and Swift range to help convert between NSString's use of NS range and Swift's string and its range. This is especially useful for classes like AttributedString and RegularExpression. In NSXPCConnection, we've added support for, better support for NSProgress. And this is actually really interesting for that first item because the new, enhanced ability for communication between extensions and apps is actually NSXPCConnection, which is available this year on iOS for the first time.
NSURLSession has also gained support for NSProgress. And so what we're hoping is that you can use all of these progress features together to flow progress from a download to the extension, to an app, and display something to your user. And finally, we've brought the thermal notifications from Mac to iOS this year.
For more on many of these topics, check out What's New in Cocoa. It was this morning, so if you missed it, we had a pretty big section on Foundation in that talk.
Performance was also a really key consideration for us this year on Foundation, and that started with a new copy-on-write behavior for NSArray and its dictionary, NSSet, and their mutable friends.
So a huge motivator for this was bridging into Swift. So when an NSArray is returned from an Objective-C API, perhaps in a framework, and you use it in Swift, you're receiving the Swift value type: array, dictionary, and set. And to preserve value semantics, those structures call copy on the reference type when they do that bridging.
If the result happened to be one of the mutable subclasses, then that copy could be pretty expensive. Now, we can defer the cost of that copy until the time that it's actually mutated, if that happens at all, which can result in really big improvements for performance when bridging. Struct data, a part of the Foundation's Swift overlap, has also gained a lot of performance enhancements. In particular, something really cool -- we're able to actually inline critical parts of data's behavior into your app when you compile. This can include things like indexing byte by byte in a data.
And again, that leads to some really impressive performance improvements.
NSCalendar has gained a better performance, both in CPU time and with lower peak memory, and in addition to that, we've actually improved the results that it gives you, especially in corner cases. And finally, we've improved the performance of bridging NSNumber to and from Swift, and we've improved its behavior in corner cases as well to provide better safety when you are converting things from NSNumber to Swift types like integer, Boolean, and so forth.
This faster bridging has a big impact on things like property list parsing.
For more on many of these topics, check out Efficient Interactions with Frameworks. That's Friday at 1:50.
Now, with that, I'd like to turn it over to my colleague Michael to talk about key paths and key value observing.
Hi, I'm Michael from the Foundation Team, and I'm excited to share some improvements that we're making to key paths and key value observing this year. And I'd like to start by saying something that we feel fairly strongly about on the Foundation Team.
And that is that key paths are incredibly important in Cocoa development. And this is because they let us reason about the structure of our types apart from any specific instance in a way that's far more constrained than a closure.
And this is so important that we felt that they weren't, that warranted special treatment in the language itself. And we started on this last year, when we added string key paths to Swift 3.
This was the, this added the ability for Swift to, at compile time, confirm the correctness of an Objective-C key path, which I'll review now. Let's suppose we have a class Kid, and it has some key value observable properties, such as their nickname, their age, and, of course, their current best friend of the moment. We can go ahead and construct an instance -- in this case, a little boy by the name of Benji -- and then form a string key path to a kid's nickname property. And then, the Swift compiler will confirm that this is a reasonable thing for us to do. We can then use key value, or key value coding to read or write that variable back into the instance.
Now, while this compile time check that we get with using a string key path expression is pretty awesome, in the end, it still compiles down to a string.
And in order for that string to be useful, we need to use the Objective-C runtime, which, last I checked, still remains unavailable for Swift value types and probably won't be made available any time soon.
Finally, string key paths carry no type information. It's just a string.
And so all general API that uses string key paths needs to be defined in terms of any. But this is Swift, and surely we can do better.
And so we thought about what would, you know, key paths, what should they be in Swift? Well, we'd want to be able to describe properties first, so that's pretty essential. They should be statically type-safe.
They should be fast as well. And they should work with all the kinds of values that we encounter in Swift. And they also should work on all platforms where Swift is supported.
And so we thought for a long time about how to make all these key paths' dreams come true, ultimately sharing our idea with the world through the open source Swift evolution process with a document entitled SE-0161 Smart Key Paths. And here's what the new Swift 4 key path literals look like.
They start with a backslash, followed by the name of the base type, a dot to indicate that we're doing something inside that base type, and then the name of the property. And the backslash here is really important because it helps us to disambiguate the execution of a property versus a mention or a reference to the property. And of course, this is Swift, so when we can infer the base type, we do, though the backslash and the dot remain.
Key paths can be composed in sequence, just like calling property after property, after property.
And soon, optional chaining will work as it does with properties.
We'll also soon allow indirection through subscripts.
Someone likes that. I like it too. Key paths can also begin directly at a subscript, and here we're starting with data's byte subscript, and we're using the startIndex here.
And of course, this can be inferred as well, though the backslash and the dot remain for consistency.
These new key path expressions offer uniform syntax for all Swift types that support properties, whether they are stored or computed.
And of course, forming key paths is one thing, but how can we use them? Well, let's suppose we have a key path. In this case, to a kid's age.
Using the key path to read a property is as easy as invoking a subscript.
That's starting to look like code, so I'm going to go ahead and syntax highlight now. There we go. So there's a little bit going on here, and I want to talk about some of the motivations behind why this looks the way it looks. First, we gave the key path subscript parameter a label, and we did this because we wanted it to be non-ambiguous with other subscripts that may exist on other types. Next, the type of the value that you're using, invoking the subscript on needs to match the base type of the key path. And if they match, it's a reasonable thing, and your code will compile. You can also use subscripts to mutate a particular value.
And subscripts are nice because they offer a fast and symmetric syntax for reads and writes into a value, whether they be a value type or a reference type. Now, let me, I've been showing these with reference types now, but now I want to switch and show how they work with value types. And to do this, we're going to expand our example to what I really wanted to talk about, which is birthday party planning.
So let's go ahead and create a party.
Benji's going to have a construction-theme birthday party, it looks like. And reading from a value type with a key path that uses the same subscript syntax that we saw with our reference types.
Similarly, mutating a party uses the same subscript syntax. There's a common theme here. The syntax really is uniform.
However, since this is Swift, we know that Ben's party is a birthday party, and so the language can infer that for us.
And I just heard that Ben changed his mind about his birthday party theme again, so let's fix that while we're at it. Now, here I'm just highlighting the syntax. In code like this, you would just call the properties, so let's look at what's actually happening when you use these key path expressions. Key path expressions are actually producing concrete values, and like all values, they can be stored.
Well, what is the type of this variable? Let's pretend we can Option-click in X, just like we could in Xcode and see that, not surprisingly, we're getting back a strongly typed key path with the base type being a kid and the property type being a string because nicknames are strings.
Strongly typed key paths also work with compound key paths. Here we see that we started a birthday party and traversed through to the celebrant's age. And of course, age is a double because if you've ever known a young kid, the numbers after the decimal point are very important.
Key paths stored in variables can be used just like the literals. And because they're strongly typed, they are statically known to have the right type -- here double, as we expected.
Let's suppose we had another birthday party to plan.
This time, it's Mia's, or Ben's little sister Mia.
We can use the same key path variable to find out which birthday she'll be celebrating, and in this way, key paths kind of serve as an unexecuted property invocation.
Now, in this example, I'm hard coding the celebrant's age, but let's go ahead and generalize this a bit. What if I wanted to know the age of anyone relevant to a party? We're going to go ahead and define a function, and we're going to call it the partyPersonsAge function. They're given a party, and a key path to a participant will return their age. And to do this, I'm going to show another feature that these type-safe key paths have, and that is the ability to dynamically form new key paths from other key paths. And so here I'll append two key paths together, the participantPath to a kid's age. And again, we're inferring kid here, which is why you don't see the word "kid" there, except for in the variable name.
And as you would expect, you get a strongly typed key path that starts at a birthday party and goes to a double.
Using the key path is the same as any other key path stored into a variable. And we can go ahead and call our function on the celebrant in that we have the same exact result that we saw previously.
And when subscripts are supported, we can also use this function now to find the age of the first attendee to the party. Now, I want to talk to the rules of appending key paths a bit. When we append two key paths together, it's like we're adding them together. But in order for this addition to make sense, we really need to look at the types of the key paths involved.
Specifically, we need to look at each base type and each property type.
And the inner types need to match. And if that's the case, we can form a key path from the original base type to the final property type. And in this way, it's like key paths actually don't care how they get from the, their base type to the property type, just that they can, and the compiler ensures that for us. Now, I'd like to take another look at another example or another aspect of the type safety that the Swift key paths provide, in case it wasn't that clear. Suppose we wanted to output a summary of our party. We could form an array of labels in key paths, but what would we expect this array, partyPaths, what would we expect the type of it to be? After all, theme is a string, the attending is an array of kids, and the celebrant is a reference to a single kid.
In this case, we get a new type. It's a, it's an array of partial key paths to BirthdayParty. And partial key paths are partially type-erased key paths. They know about their base type, but they can point to any valid key path for that particular base. And so in this case, let's go ahead and print our report.
We'll zip our titles and paths together, use the party path to get the party value, and then print our report. And you can see Mia's having a space-themed -- well, it looks like a family birthday party -- but it's space themed, which should be pretty fun. Now, I want to add an extension to BirthdayParty. We're going to add a function that lets our kids blow out their birthday candles, and this is going to be a little bit different from what we were doing previously because up until now, we've been reading key paths, and now I want to write to a key path or use a key path to write to a value. So we're going to add our functions, and I want to point out that age key path is actually a new type. It's a writable key path of a, that starts at a BirthdayParty and goes to a double. We can use writable key paths just like regular key paths to get values out of our values, and we can also use them to mutate our values.
And we can finally go ahead and blow out our candles. This all looks pretty good, except for one problem.
Doesn't compile. This is a birthday catastrophe.
So let's try to, I'm going to debug this live on stage. Debug. So the compiler's telling us, "Cannot assign to an immutable expression of type 'Double,'" which is very Swift of it. Let's see if we can figure out what's going on. It's saying immutable, but we are passing a writable key path. And we are indeed passing a key path. Let's confirm, though, that this key path is actually to immutable variable. That, you know, sometimes you say let when you meant to say var. So let's bring back the declaration of kid, but we'll see that var, our age is indeed mutable. It's a var, so that's not the problem.
So maybe the problem's with the actual write itself. And that's, and we're using the subscript. And I'm on stage telling you that subscripts work, so that's not the problem. So it must be something to do self. Well, what's self? Self is an extension on BirthdayParty, so now we're going to need to bring back the declaration of BirthdayParty. Luckily, we had some room. And we'll see that BirthdayParty's a struct, and structs are value types. And so the compiler's actually doing the right thing here. It's not letting us mutate a BirthdayParty because our key path's anchored in a BirthdayParty there.
And so one trick we could do, right. We look in our bag of Swift tricks that we know, and we see that, oh, OK, we can just add mutating, and everything will work.
But when you do that, you want to stop and think, is this really the right choice? Because we're not really modifying the key, or the birthday party. We're modifying the celebrant. Birthday parties don't have ages last I checked, and celebrant's actually a class, a reference type.
And so for this, we actually have another kind of key path that adds reference mutable semantics to mutations, and it's called a reference writable key path.
And so let's go ahead and use it. And this compiles, and we can finally assert that our little boy, Benji, is finally one year older, although I think he goes by Ben now. And so we can review the difference between these two kinds of mutating key paths. So we have a writable key path. And writable key paths write directly into their value-type bases. So the base or the chained bases need to be mutable. Whereas a reference writable key path simply invokes a property setter on the reference type. And all of these key path types form an inheritance tree, each more specific than the last. Rooted at the top of this tree is another kind of key path that I haven't talked about called an any key path, and this is a fully type-erased key path. And this is useful for when you have key paths that are comprised of multiple bases to multiple different property types, usually in a collection. Now, if all of this seems a little bit complicated, I assure you that the rules for the kind of key path that you want, and use, and get are actually fairly simple and match the rules that you're already familiar with from working with Swift value types and reference types.
We'll start out with eliminating half of the problem. Read-only properties always yield a key path. With read-write properties, things get a little more nuanced.
Mutable value type bases or chained mutable value type bases will result in a writable key path. And so writable key paths help let you write efficiently into a value type. However, if one of those value types is made immutable, say through a let statement, the mutability of that property goes away, just as it does when you're using regular properties, and you're left with just a key path.
And saving the simplest case for last, read-write properties on reference type bases always produce reference writable key paths. Now, I want to share one last detail regarding the behavior of key paths.
When we can use key paths with subscripts, it's important to know how their behavior differs from closures.
Consider the following example.
Here I'm going to form a key path to the first attendee of the birthday party and use that to identify their, use our partyPersonAge method from before to identify their age. And not surprisingly, the key path that we get here is actually, is going to the zeroth element of the attendees array.
Well, let's suppose I change the index to 1. I also care about the second attendee's age for some reason.
You would be, might be surprised that the resulting key path actually is unchanged, regardless of my changing of index. And in this way, key paths became different than closures. They capture by value, and so when this feature becomes available -- I wanted to say this today so that you're not surprised, and now I have.
So at this point, we've seen many examples for how these type-safe key paths satisfy our goals for fast, type-safe and expressive property traversal. I'm going to change gears a little bit now, though, because I want to talk about how these key paths can be used to improve existing APIs in Swift today.
Specifically, I want to talk about how we applied them to key value observing.
As you probably know, KVO is Cocoa's way of allowing objects to establish relationships to be notified about changes in their state. And if you've tried to use KVO in Swift up to now, you probably know that it leaves a little bit to be desired.
Let's suppose we have a reference-- Don't clap yet. Clap in a minute. Let's suppose we have a reference to an Objective-C value -- say the little kid, Mia, from earlier -- and we really care about when this kid's age changes.
We think that forming an observation should simple, look as simple as this. Now clap. So I want to call out some details about this form. We are forming our observation directly on the value type using our new type-safe key paths. What we get back is an observation token similar to our Notification Center APIs. And this observation token is doing two things for us. One, it's saving us from having to deal with unsafe raw pointers with context to uniquely identify our observation. Now, our observation is directly tied to our, the observation that we get back.
Two, it's managing the life of our observation. And so if I, if -- well, in this case, I can't set it to nil -- but if I were to set it to nil, the observation would be toward now , which is a vast improvement over getting exceptions and having your app crash when you forget to unregister an observation.
Finally -- and this is perhaps the nicest part -- you now handle your observation's reaction with a closure as opposed to nested if statements looking at and comparing strings. Let's take a look at the parameters to this closure a little bit more.
It has two parameters. The first is the observed object itself. This is the same reference, Mia, but we provide it as a parameter to help you avoid accidentally creating retain cycles.
Second, parameter is a change object, and this is very similar to the existing KVO API, except you, if you've used, if you're familiar with that API, you know that that's a loosely type dictionary, and here we're actually providing a strongly typed struct. So we know observed is a kid because of the key path and we know that the age that's changing is a double because of the key path. And now, I want to go ahead and make this real. So we'll do an example. Let's suppose we have a controller that cares about when our kids get older. It's the KindergartenController, and it has a single key value observable property, its representedKid. And we're going to form an observation, so let's go ahead and add an i var for our observation. And then, we'll form that observation right now to our controller's representedKid's age, and we'll hold onto that in the i var.
And we'll add our super-secret business logic. And if you're looking at this and thinking that it's wrong, I assure you that once you're ready for kindergarten, you're always ready for kindergarten, so it's actually correct. And that's it. That's the entire declaration of our controller.
There is no need for a dnit where I throw away or tear down my observation because it's tied to the life of that observation token. And so when the controller goes away, the observation token will go away, and it fits on the slide. So let's go ahead and create our controller.
And here we'll point to Mia, and we'll have Mia blow out her candles with our function that we defined earlier. And we'll finally see through the power of these new type-safe key paths our little girl grow one year older. Now, at this point, I've shown everything that I'm planning on showing, but I do want to circle back and talk about string key paths one more time. These guys are going to continue to exist, and they're going to remain useful for legacy APIs that, you know, insist on using strings.
However, starting in Swift 4, you now have access to these new type-safe performant key paths, and we brought them to the language because we feel that they are so incredibly important. And they're only going to grow to be more important over time. And with that, I'd like to bring Tony back on stage to discuss another important language feature that we're bringing to Swift this year. Thank you.
All right. Thanks, Michael. So next up, we're going to talk about encoding and decoding.
So broadly speaking, encoding and decoding is about the conversion between your native and custom Swift data structures and archived formats, especially JSON.
Now, many of you have told us about the challenge of a mismatch that is between the strongly typed language of Swift and loosely typed archive data formats like JSON.
We believe that the answer to this challenge is something that starts with the language itself and also takes advantage of the compiler, the standard library, and Foundation to make interaction with JSON simple, but also to provide you the opportunity for powerful customization.
So let's get started by looking at an example. Here is some JSON from one of our favorite sites, GitHub.
This is the result of asking for information about a commit made to a repository, and it's pretty standard JSON. It's a JSON object or maybe what we would call a dictionary. It supports and arbitrary number of key value pairs -- in this case, name, which is a string; email, which is also a string; and date, which is a string. And the reason is because, of course, JSON has no native type for dates. But there are many conventions by which dates are encoded in a way into JSON, and this one appears to be iso8601.
If we were to represent this JSON in Swift, it would look very different. For example, we would make a strong type for it, a struct perhaps called Author.
This struct would have exactly three properties -- name and email, which are strings still; but date, as you can see here, is using Foundation's date type. And the reason that's important is because, as you interact with the rest of the Cocoa SDK and other APIs, you'll find that date is the kind of type used to represent a point in time.
And so this is where we've reached that challenge, right.
How do we convert between that loosely typed JSON on top and the strong Swift type on the bottom? Well, we think it should be as easy as this. Simply adopt a protocol on your struct and let the compiler, the standard library, and Foundation do the vast majority of the work for you. Thank you.
So let's turn this slide into some real code.
First, I'm going to turn that JSON into a string using Swift 4's cool, new string literal syntax, the triple quote, and then turn that string into data using UTF-8 encoding, which is pretty common for JSON. Struct Author remains the same, of course. Next, we create a decoder. This is what is actually doing the conversion between the JSON and our Swift structure. We tell the decoder about that convention, the iso8601 date. And we'll talk more about this later.
And finally, we ask the decoder to decode an author. The result is not an any. It's not a dictionary. You don't have to fish through strings or check for keys. It's already the type that you care about using, in this case.
So that was pretty easy. Let's bump up the difficulty level one notch. This JSON is actually part of a larger set of JSON that comes as the result of this request, which includes things like URLs, and additional strings, and integer values.
So in Swift, we can just follow suit. So I'm going to nest my struct Author in a new struct called Commit, which is also codable.
There you can see that I'm able to use Foundation's URL type and our struct Author. So you can see how we can recursively descend into types, if they conform with codable, to decode them as well.
The string, which is, message, which is a string, and our comment count property. And to decode this, again, one line of code. We're going to decode a commit this time. And the result: Our strong Swift type, which lets us use the Swift language features that we know and love to get at the values that we care about in the archive. In this case, it's simply property access. So let's look at what's going on here. First, the codable protocol, which is actually not one protocol, but two. The first is called encodable and has one function, encode to encoder.
The purpose of that function is to allow the type to tell the encoder all of the information that it needs in order to recreate itself at a later time.
The corresponding protocol, decodable, has one initializer.
The purpose of the initializer is to allow the type to get the values that it needs from the decoder and then use those to create a fully initialized instance of itself that is ready for use. The primary design point of these APIs is to use a Swift behavior that you may already be familiar with, and that's called protocol extensions.
So in Swift, protocols can not only define an interface, but via an extension, they can provide a default implementation for that interface.
And they let you write your own implementation for either part or whole of that protocol to customize the behavior.
So let's go back to our commit to see how this works.
When I adopted the codable protocol, the compiler actually generated an implementation of encode to encoder and initfrom decoder for us completely for free. And in this case, I don't need to customize anything about them, so I can just omit them completely from my type.
There is one thing I do want to customize about this type, though, and that is the name of this property. Now, you may notice that it is using snake case, which is pretty common in JSON, but it doesn't match Swift's naming conventions. So let me show you how we're going to fix that. First, there's one more thing that the compiler generated for us, and that is this private enum called CodingKeys.
This enum is backed by a string and adopts a CodingKey protocol, which, again, we'll talk more about later. But what's interesting to note here is that this enum has four case statements that match the names of my four properties. And so in order to customize the name of my property, I just need to customize the name of my case statement. So to do that, I'm going to change this comment count snake case to camel case.
But as you can see, I remain compatible with the JSON that we're reading by setting the string value of that case to be the value we expect to find in our archive.
Now, if that's all the customization that we needed to do, then we're done. Maybe you can stop watching now and leave, but by the end of the talk, I do want to show you how we can do even more kinds of customizations on this commit. For now, I'd like to hand it over to my colleague Itai to show us a demo of this stuff in action.
So Tony showed you just how easy it is to adopt codable in your types, but let's dive in to see what this might look like for many of your apps in practice. I've got a small app here that I've been prototyping lately. Because I'm such a big fan of Swift, I like to watch for interesting git commits as they come in on Swift's GitHub repo. I've written a small app here that talks to GitHub's JSON REST API to parse these commits and show me them in the table view.
So let's take a quick look at how easy it was to put this app together using the new codable APIs. If we switch to Xcode, you'll notice some of the same models that Tony had up on his slide expanded a little bit. We've got the same commit info, author info, and we've gone ahead and done the same renaming.
On the right is the JSON spec that GitHub provides, but with some of the irrelevant parts snipped out. And if you'll notice on the bottom right, we've got some info in the JSON spec that we're not currently decoding.
That's actually OK because it'll get ignored by default, and so we can come back to this later. So let's hide this JSON spec and go a little bit further down into our file to see how we can use these models in practice.
So in here, we've got our CommitsViewController, and this is the view controller that actually displays these commits in our table view. And so the view controller here has our table view, along with an array of these commits. And note here that this is an array of our type. It's not an array of any or anything similar.
When we're going to go and display this data, we can fetch the data from GitHub, and then using a JSON decoder, just like Tony showed you, we can go ahead and request to decode an array of these commits into our type.
Once that's done, we can reload our table view and have that display.
Now, if anything goes wrong, we can catch that error and display some localized information to the user to tell them what went wrong at a high level. Now, this is how you load the data into your app, but let's take a look at how this hooks up to our UI. So a little bit further down in the file here, I've got a helper method that lets me set up table view cells right before they're displayed to the user. So in here, to set up my custom table view cell, I'm going to pull out a commit from our array of commits, and then using the strongly typed properties on that commit, we can hook it up to our UI. And note again that we're not downcasting from any and we're not peering through arrays or dictionaries. This is our type the way we wrote it and how we want to use it.
Now, that's all nice and good, but let's go back into our app and you'll take a look here and notice that we've got some room in the UI that I've left here for hooking up the hash values of each of these commits. But it's not hooked up yet, so let's go ahead and do that now.
If we go back to our models and pop open the JSON spec again, we can see that there's a hash property in the JSON spec that we haven't been requesting. So let's go ahead and add that to our type.
And then, if I go ahead and build our project to use it, you'll notice that I actually got a build failure. So let's explore why that happens.
As part of this type, I've created a custom CodingKeys enum. Now, the CodingKeys enum that you put in your type is a really powerful tool for controlling what the compiler generates as part of init from NNCode 2.
In this case, I provided a CodingKeys enum that renames my info property to commit to match what's in the JSON.
But in this case, the hash property that I just added isn't found in the CodingKeys.
Now, what the compiler will try to do is if you purposefully leave a property out of your CodingKeys enum, it'll actually omit it from your encoded and decoded representation. But what's happening here is that because this hash property doesn't have a default value, if the compiler were to try and generate an initializer for us, there'd be no reasonable value to initialize this property to, and so the compiler refuses to do it and we get our build failure because our type actually doesn't conform to decodable.
Now, in this case, we actually don't want to leave this hash property out of our encoded representation. We do want to decode it. So let's go ahead and mirror that same property in our CodingKeys enum.
Let's hide the JSON spec again and go and hook up this property directly to our UI.
So here in the cell setup method that we have, we're going to add another line of code that grabs that hash, and here we're going to shorten it up a bit so it fits nicely in our UI. And just like using everything else, using the strongly typed properties, we can hook it up to our UI directly.
So let's go ahead and rerun our app and take a look to make sure that things hooked up correctly.
Here, now that we've rerun, we can actually see that everything is hooked up to our UI, and I'm pretty happy because that took a whole of four lines of code to add to our app.
Now, going back to the code for a moment, let's take a look at what we can do when things go wrong in our app. So if we pop open the JSON spec one more time, you might notice that in the bottom right, we have one final property that we haven't been decoding, so let's go ahead and try to do that now.
We're going to add a URL property to our type, and again, we're going to want to mirror it in the CodingKeys enum.
This time, though, let's give the CodingKey a value that is clearly not found within our JSON payload.
Now, when we go ahead and try to decode this value, it won't be found, and so this'll actually be an error at decode time. To see how we can handle that error, let's hide the JSON spec again and go down to where we perform the decode.
In order to handle this error, we can catch a decoding error, key not found error, which indicates that we tried to access something with this key, but it wasn't found in the payload anywhere.
Along with that, we get some contextual information about what went wrong and where.
So now, let's set a break point here, and run our app with this faulty key, and take a look to make sure that we can catch this error and we hit the break point.
And so now that we run this app, you'll notice that we do hit that break point. Now, if we go ahead and print the key, you might see that this is in fact the URL key that we gave a faulty value to, and in fact, it wasn't found in the payload, and so we get the error.
Now, here if we take a look at the contextual information, you might see two useful features that helps you debug what happened.
First, a debug description for you the developer to figure out what went wrong along with the coding path that describes where in the payload something happened to cause this to go wrong.
Now, this is all nice and good, but in fact, if my URL is something I don't really care about all that much and I might not need it, one way to handle this error is to make your URL optional.
When you make the property optional by default, if the key or value is not found, it'll actually get set to nil on initialization.
So let's hide our UI a bit and rerun our app to see if we hit that break point or not. And in fact, when we rerun it, we don't hit that break point because the value is set to nil by default, which is a handy behavior.
Now, let's go back to our code and take a look and see what other errors we might be able to catch that are helpful.
One other error like that is the DecodingError.valueNotFound, which indicates we tried to decode something of this type but in fact found nil.
Again, you get that same contextual information that tells you what went wrong and where. Along with that, you might want to catch a type mismatch, which indicates that you try to decode something of this type, but something else was found in a, in the payload. Say, you tried to decode a strong, but instead, a number was found. And again, you get that same contextual information as before.
Now, these errors are really handy for when you want to debug when something goes wrong, but in the general case, you don't really want to catch these at the top level like here. Instead, you just want to capture general error and display something localized to the users so they can figure out what went wrong or report the bug. Now, these are actually a very powerful tool to do some more advanced things. If you customize your init from or encode to, you can actually catch these errors within your types to do powerful things like data migrations, renaming properties, and so on and so forth. But within our app, we actually don't need that because I've got exactly what I want with not much code. And so I'm going to turn things back over to Tony to talk about some of these more advanced encoding and decoding topics. Thanks.
All right, let's move on to talk about some more advanced topics in this, with encoding and decoding. And to do that, we're going to go over what I call the three pillars of our codable API design philosophy. The first is that we really wanted error handling to be built right in, as you just saw in this demo. So when you're working with archived data, dealing with unexpected input is not a question of if, but simply when.
This can be data corruption, it could be unexpected API changes from where you receive that data, or even something like malicious input -- somebody trying to probe for weaknesses in your app. And so we decided that there should be no fatal errors as a result of parsing untrusted data. However, we do use the fatal error in Swift if we detect something that may be a developer mistake. And in those cases, there's a string with the fatal error that'll tell you where you may have gone wrong. For everything else, we use Swift's built-in error handling mechanism, and those kinds of errors are possible on both encoding and decoding. So let's look at what they are.
First, there's encoding. So there's only one kind of error on encoding, and that is an invalid value.
So for some kinds of formats like JSON, we wanted to give them the flexibility to handle input that they may not expect without resorting to a fatal error or some kind of default value. In JSON, for example, not a number or infinity are not valid values. And so in those cases, they can throw in error. There may not be much you can do about this at the type, by type level, but you could still catch it at the top level and present an error to your user or prevent some other kind of recovery mechanism.
On decoding, there are four kinds of error, three of which we just saw in the demo -- type mismatch, missing key, and missing value, which you can handle, again, either by using the air handling mechanism if the a required part of your type or by making those properties optional.
And lastly, we have data corrupt. Data corrupt is our kind of catch-all error for all the other kinds of things that can happen during a decode. And to see where it might be useful, let's go into some depth on what actually happens during a decode.
First, in the beginning, all we have are bytes. It could be from the network. It could be from a file on disk or somewhere else in your app. But regardless, at this point, we don't really know anything about them. And so the very first step is to convert those bytes into structured bytes.
For example, the JSON decoder has to verify certain requirements of the JSON spec are met -- the particular bytes at the beginning of the archive, which indicate string encoding; certain characters which are used as delimiters for strings, numeric values, arrays, and dictionaries, and so forth. If any of those look wrong, then the JSON decoder can throw an error and stop the decode right there.
After that, we want to convert from things like JSON arrays, and dictionaries, and strings into your types, commits and authors. That is, after all, the entire point of this API.
But there may be more that we can do, and we call that domain-specific validation. For example, let's say that you have a type that has an integer property, but the only valid values for the integer are between zero and 100.
Or maybe your type has two Boolean properties, but they have to have an exclusive or relationship with each other.
These kinds of things can be difficult to express in Swift's type system, but we do think we have a great tool for handling those, and that's just simply writing more Swift code. And so we wanted to make sure that we provided you the opportunity to do that if you have those kinds of requirements.
Finally, you may have graph-level validation. This is about the relationship of the objects in the graph to each other or to another part of your app.
Let's apply this to our commit. So earlier we saw how we customized our commentCount property by customizing the enum called CodingKeys. Now, we're going to customize the decodable by implementing init from decoder.
First, I asked the decoder for a container. Containers are what match up your keys to the values that you expect to find in the archive.
Once we have a container, we can ask it for the values that we need. So in this case, a URL, a string, our author, and there's our recursive descent again, and the integer value for commentCount.
Now, let's say that I have an additional requirement that I need to verify as part of my spec, and that is that all URLs have to be HTTPS. If they're not, then something has gone wrong.
So let's see how we might do that. First, make some more room on the slide.
After that, I'm going to use the URL API that we already know how to use, and that is the scheme property.
Here I can just check that it's equal to HTTPS, and if it's not, I can throw one of these decoding errors, providing a debug description so that you can catch it in your debugger as Itai showed in the demo.
Now, what you see, what you'll notice is not here is this type, commit, looking into the string value for the URL. We can allow URL to decode itself, and that's part of what's so great about this design. So URL knows if that string is URL or not, and if it's not, it'll throw an error before we even get to this point. And because of the design of Swift's error handling, that can propagate out of this type to the one that's decoding the commit or even to the top level. Let's move on to the second pillar, and that is encapsulation of the encoding details.
We felt it was really important to make sure that the keys and values that a type chooses to put into the archive are private to that type. And the reason it's important is because that frees us from something that, from designing something that has global knowledge of everything in the archive that can possibly be there.
The main mechanism we have for performing this encapsulation is called containers, and we have three kinds. The first is a keyed container.
Keyed containers are the preferred choice in the vast majority of cases, and the reason is because they're by far the most forward and backward compatible.
If in a new version of your app you have new or changed data, you simply have to use a new key.
This makes the most versions of your app compatible with the most versions of your data, which is the best possible outcome for everybody.
Let's look at what those keys actually are. So earlier we talked about the CodingKey protocol. Here's what it is. It has two protocol, or two properties and two initializers. So the properties are stringValue, which is handy when you're working with JSON, for example, but you can also provide an integer value, which is useful for formats that may support a more efficient encoded binary representation.
The initializers, what I would like you to notice is that they are optional. What that means is that the decoder has an ability to perform an additional level of safety checking. It can verify with your coding key that the value that's found in the archive is actually what you expect to find there.
Typically, you're going to adopt this protocol on an enumeration, like the one we've seen so far. And again, what's happening here is that the compiler in the standard library are providing an implementation of all four of those requirements for us completely for free. So in this case, because the enum is backed by a string, the compiler uses the case name as the string value, both for the property and for the initializer. The intValue, though, remains nil because there's not enough information in this enum to assign a particular value to that.
Earlier when we customized the case name, you can see how that worked now. We changed the name of the case, but the value remained the same. And so stringValue remained compatible with our GitHub API.
If you're writing library code, I would encourage you to consider backing your coding keys with an integer. If you do this, you still get more implementation for free from the compiler -- in this case, an integer value, which, again, could be useful for formats that may support integer keys.
We also support unkeyed containers. These encode and decode in order.
Use these for ordered or unbounded data, and, you know, the reason for that is that, of course, you don't have to generate fake keys in order to get your data into an archive.
We also support single value container, which, as the name suggests, holds exactly one entry.
Use these for primitive types. For example, date stores the number of seconds since a reference point in time.
Now, when you choose these, just be aware that they are the least compatible choice, so keep that in mind.
Let's return one more time to our commit.
So we saw how we customized the commentCount and the CodingKeys with that, the decoding by changing init from decoder. Now, let's look at encoding with encode to encoder. And actually, I don't need to customize anything here, but I still want to show you what it looks like so you can understand how it works. So first, we get a container, and that container, as you can see here, is keyed by our own private-to-us CodingKeys. That container is how I can encode the values that I want to be put in the archive -- our URL, message, author, the recursive descent again, and the comment count. I do want to show you an example of where you may choose a different kind of container, so let's say that we are working with the GO JSON spec, which has this concept of a point. And point has two values, and it should be an array of two numeric entries in JSON. So in order to make that work, I'm going to adopt encodable and implement the encode to encoder to use a unkeyed container. And you'll notice, of course, there's no key argument to this container. And when I encode, I use no keys. And the result looks something like this in JSON.
We also support nested containers. So let's say that maybe the second entry in my dictionary actually needed to be an array of three values. So we support nesting unkeyed containers and keyed containers, as you see here, or any other combination of keyed, unkeyed, and single value.
The primary use case for nested containers is actually classes. We've talked a lot about structs so far, but nested containers gives us a natural mechanism for encapsulating our superclass data from our own data as a subclass, which is a change from NSCoding.
Let's look at an example. Here's everyone's favorite object-oriented example, animals. So animals have a leg count, naturally. And its own coding keys. And here you can see that on this class, when I implement init from decoder, it is a required initializer.
Here I create a keyed contained using the animal's coding keys and decode my leg count. Pretty similar to what we've seen so far.
Now, let's subclass it.
Dogs is a kind of animal that has a best friend, which is the kid from our birthday party earlier.
Now, you notice that dog also has a private enum called CodingKeys, and yet, even though it has the same name as the one from the superclass, because it's private, it doesn't conflict with the one that animal uses. So when I implement the dog's init from decoder and get a container with its own coding keys, I can decode it in a type-safe way with the keys that are important to it, not its superclass.
Now, we do need to finish that nesting.
So we could call superclass, our superclasses init from decoder with the decoder that we received. However, that doesn't give the container a chance to nest that superclass data. So the easiest way to do that is to use this convenience API -- it's called superDecoder -- that gets a new decoder that we could pass to our superclass. And by calling super, we finish satisfying Swift's rules for creating an initializer that results in a finally initialized type that's ready for use.
Finally, we have our third pillar, and that is abstracting the encoded format from these types.
We felt it was important to be able to reuse one implementation of these protocols.
We didn't want to wind up in a situation where we had many almost duplicated implementations of encodable and decodable to support new formats.
So by abstracting the format, we can allow brand-new formats without any library changes. Those formats can come from us, or from you, or even from Swift Packages, and those formats can work with types that come from us, or from you, or with Swift Packages.
We do understand, though, that different formats have different fundamental types and different conventions.
So the mechanism we have for working with that is called encoding strategies. These are encoder- and decoder-specific customizations for certain types. For example, in JSON, we saw one already for date. In our GitHub example, the date was encoded as an iso8601 string.
But there are other conventions that are possible. For example, the number of seconds since a reference date, the number of milliseconds since a reference date, or you can even specify a completely custom date formatter if you have something very specialized in mind.
The JSON encoder and decoder support other kinds of strategies. For example, for data. In JSON, it's very common to Base64 encode your data.
But we also allow you to customize this by choosing a strategy that encodes it as an array of bytes, or you can specify something completely custom, like this one, which turns all zeros into sheep and everything else into a dog.
I don't know why you'd do this, but it's possible, so there you go. Now, this abstraction helps us with different formats as well. So we've seen, we've talked a lot about JSON today, but actually, we are also introducing a property list encoder and decoder.
And property lists, unlike JSON, have native types for data and for date. And so when these encoders and decoders encounter these objects either in the object graph that it's encoding or in the data that it's unarchiving, we can convert them into the right types that are proper for that format. Because of these abstractions, we're able to adopt the codable protocols on a wide variety of Foundation types, including all the ones you see here. Now, we've talked about a lot of codable API. I want to give you a visual overview to help you understand what the big picture is. So we're going to start, of course, with your type.
Your type adopts two protocols. They're called encodable and decodable.
These have a function and an initializer, which give you access to an encoder and a decoder.
These provide you access to containers, and that's what actually holds the values that are in the archive. In the case of a keyed container, we use the coding keys that are defined by your type.
And finally, the encoders and containers provide an abstraction for encoded formats like JSON, property list, and more. All right, so we started today by going over some new API and improved performance in this year's release of Foundation. After that, we looked at the new strongly typed key paths for Swift, including one really cool use case, our brand-new, closure-based KVO API.
And finally, we went over the new codable protocols, which make integration with formats like JSON easy, but also allow you the opportunity for powerful customization.
For more information, check out this link.
We have a couple of related sessions that we've talked about. Thank you so much.
[ Applause ]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.