File Provider Enhancements

Session 243 WWDC 2017

File providers are front and center in the new Files app and system-provided document browser, empowering users to work on their documents in the cloud without the confusion of multiple copies or different versions. Hear details about how your cloud services can integrate with this new functionality. See how to provide an efficient and seamless user experience. If you are an app developer, learn the details about how file providers work and hear about exciting opportunities to take advantage of service-specific APIs from cloud storage providers.

[ Crowd Sounds ]

[ Applause ]

Good morning, and welcome.

My name is Jean-Gabriel.

I hope you've been having a great week.

We're here to talk about file providers, the technology between, behind the new Files app.

We're going to see what file providers are and what new APIs in iOS 11 allow you to enumerate files from the cloud down to the device, to modify those files on the device and up to the cloud, and to customize your file provider to expose your non-standout features.

So file providers have been around since iOS 8, and at the core, they materialize files for applications.

So applications may keep preferences to files that are provided by file providers, and when they need the files, file providers fulfill the promises.

But we are here today to talk about the new APIs, the APIs for enumerating files from the cloud down to the device.

So the audience for this talk is people who have servers out there with user documents on those servers and want to bring those documents to iPad and iPhone.

So let's look at the Files app.

The Files app is a thin wrapper around a new piece of system UI called the document browser that is pretty ubiquitous in the system.

It is where the user, where users find, organize, share, open documents.

File providers appear here under Locations.

These are the entry points to your cloud storage, and iCloud Drive is one of them.

That's good.

That means that the APIs that we are going to go through today are implemented by iCloud Drive, really well tested, and you have a reference point if you're wondering about the expected behavior of, say, renaming your file when there's already a file with the same name and the same folder, or you can just try it out in iCloud Drive and see how it's supposed to behave.

It also means that your documents, being in a file provider just like iCloud Drive, are first-class citizens on the OS.

They're available everywhere really prominently just as iCloud Drive documents.

Actually, on this shot, iCloud Drive is at the top, but the user may want to reconfigure that however they want to really leave on your file provider.

There's on file provider in there that I just want to make a passing mention of.

On My iPad, that is the only local file provider.

That is the only file provider that doesn't show files that are in the cloud somewhere, and it's for apps to expose their document, their local document storage to other applications.

So let's see where the document browser fits on the platform.

Well, it sits right below the document-based applications the Files app that we just saw; all the iWork apps, which [inaudible] the document browser to be that new piece of system UI; and all the third-party [inaudible] in iOS 11.

On the other side of the document browser UI are the file providers, and the file providers will provide files to the system UI while the document-based applications pick, open one of those documents from whichever file provider, edit, or create new documents.

So the reason why document-based applications are going to adapt is that this is a single API to get access to all the places where their users have their documents.

Replaces the multiple SDKs that they have had to adapt to until now.

The reasons for file providers to adopt is the privacy model, which is really awesome here.

So document-based applications open documents one at a time.

It's totally user-gated access.

The system UI is out of process.

Document-based applications cannot know which files exist next to the file that was picked or even which file providers are installed until the user picks one of the documents.

There are two key APIs here: UIDocumentBrowser ViewController, which is in the SDK of the document-based applications, and NSFileProviderExtension.

We're not here to talk about UIDocumentBrowser ViewController.

That was covered in the session yesterday that I invite you to go watch if you missed it about building document-based applications.

We are here to talk about the other end of this, NSFileProviderExtension.

So let's look again at the Files app.

There are two main ways for the user to go and interact with his documents, and those correspond to two tabs in the app.

The first tab is the Browse tab.

That is where the user goes to find content in your cloud and bring it down to the device.

In this mode, your file provider extension acts as a pipe between the system UI and your servers.

It's not even expected to work offline, really.

Once the user has selected those documents in the Browse tab, they become available in the Recents tab.

The Recents tab is the other mode, and it is a flat list of documents that come from the very sources of documents and are really relevant to the user.

That is where the user is going to go look for the file he wants to open.

And sure enough, it's going to be there.

So two tabs Browse, Recents.

The Browse tab is for putting new content for file providers online for, folder, online to folders and pick a document.

The Recents tab shows what we call the working set, and we're going to talk about the working set now.

So it's not folder based and it spans multiple file providers.

And in a working set, there are these documents.

So recently-opened documents or used document, favorites, tag documents and folders, documents shared through or by the user, documents that were previously downloaded to the device and are available offline, and recently-deleted documents.

For the user experience to be consistent across file provider, we ask you to put all those files in the working set.

But you have domain-specific knowledge.

You know what's relevant to the user.

So you might choose to add more to the working set, if you so want.

However, caveat: We are actually Smart Caching the working set on the device.

So once something is in the working set, you'll have to keep it in sync.

I'll get back to that.

So in the document browser in the Recents tab, you'll see the working set.

And each file provider may contribute different documents to the working set.

We don't do that at display time.

If we had to merge the list of documents as they come in from the different file provider extensions when the UI is onscreen, you'd have weird animations, visual glitches.

So we don't do that.

Instead, what we do is we enumerate a working set index that is kept by the system when the UI comes up.

And your file provider extensions are not even invoked at all.

We're going to talk about how that index is built.

So the index is local and on the device.

It is the aggregation of the working set from the different file providers.

Because the index is local, it means the index is available offline.

It means that offline, if I bring up that UI, the document browser, I will see my recent documents.

That doesn't mean that all the documents are local on the device at all time and offline.

It means that [inaudible] to build the UI is cached to the device.

Because the index is local and available offline, it is also in Spotlight.

So the system index is for you, the working set in Spotlight, and users may pick a document in Spotlight, and that will wake your file provider extension at that time.

So let's talk about how you provide files to the working set index, which happens in the background before the document browser comes up.

The first thing that you are going to have to do is sync your working set from your servers down to the device.

So watch those file provider extension boxes here.

The, we're asking you to keep a database of the metadata for all the files in the working set on the device.

And actually, do sync both ways because there are local changes and [inaudible] changes.

The second step is that the working set, the system is going to drive the enumeration of the working set from your file providers to build the index.

Happens in the background.

And then at a later time, when the UI comes up, the system will enumerate the working set index without waking your file provider extension to show the Recents in the document browser.

All right, we're asking you to do a lot of things here.

We're asking you do sync and to do sync of a partial set of your documents.

If you can't do partial sync, you might want to consider syncing all the user's documents down to the device.

You're going to hit scaling issues, so think carefully about what you do there.

OK, what we saw to this point is that there are two ways to browse documents.

There is a Recents tab that shows the working set, which is synced in the background to the device, and there is a Browse tab, which is based on the online enumerations of folders.

And I'm going to call Pierre to show you how to create a file provider extension now.

Thank you, Pierre.

Thank you, Jean-Gabriel.

So let's talk about creating a file provider extension.

First, we're going to see how to create the project, then how to provide a single item, then enumerate a list of items, and then I will [inaudible] was going to go through you, with you an [inaudible] to modify item, provide custom action, and as well as do something very, very powerful, which is provide services to third-parties.

What does that mean?

It means that your file provider extension can provide its own API, so you own API to other third-party's app.

This is really cool, so you probably want to stay until you see this or play it online later.

Creating a file provider extension is really simple if you start from scratch.

So there is a Xcode template, File Provider Extension.

Just click on it.

Boom, it's done.

And then the real work begin.

You have to subclass NSFileProviderExtension.

If you already have an existing file provider extension, in that case, what you should do is add the support enumeration key to your Info.plist.

That will tell the system, look, you probably want to take this file extension and use the new UI to render my document.

So the big picture during this file provider extension subclass that you should build.

Your file provider extension subclass [inaudible] is to vend the item metadata by implementing or returning object implementing the NSFileProvider protocol, NSFileProviderItem protocol.

Those item are identified by a string that you provide and are materialized on disk through file URL.

The folder contents of your file provider is exposed by implementing what we call an NSFileProviderEnumerator.

That sounds complicated, but it's not.

So, and before jumping into the very detail, let's have a big picture of the tasks we've been going through and see what the anatomy of a file provider extension is going to look like after we build all this.

So first task, we need to provide the metadata for a given item identifier.

That's straightforward if you have a database, so you probably want to have a database.

Then, you need to map URLs to item identifiers.

That's probably a semicolon in your database.

Last, you're in charge of managing the disk storage, whether the data corresponding to a file is local or not.

That's the second piece of your file provider extension.

And inside the disk storage, of course, file can be there or not, as [inaudible].

OK, so we've seen the anatomy.

Let's jump in and provide an item.

What does it mean to provide an item?

First, you need to provide a file URL.

Then, file contents, metadata, and thumbnail for your item.

That's it.

Why do you need to provide a file URL, you might ask?

Well, file provider manipulates file provider item and file provider item identifier, not file URLs directly.

However, document-based apps do consume file URLs, so we need to bridge between the two, so let's take an example.

When the user taps on the file in the File app, the File app only has access to your item identifier at that point.

So before calling the document-based app, it's actually going to call into your file provider extension subclass.

The method urlForItem with identifier is going to get called, and so you will look in your database, figure out the URL for the given item identifier, and, boom, you'll return into the document-based app.

Not sure if the system does it for you, but you [inaudible] to the system which items to the document-based app, and you're done.

At that point, the document-based app opened a file by URL.

On the other [inaudible], I was talking about the database to the URL to item identifier mapping.

One thing you might want to do is to actually start the item identifier inside the file URL.

It's very convenient because you can avoid database lookup if you do that and you can tag items between renames.

Last tidbit, you probably want to keep a flat list in your disk storage.

You don't need to replicate the folder hierarchy because this is quite complicated, and so you can avoid it, so please do.

So you might say, hey, Pierre, so we have the file URL, but what about the file content?

Because I did not know they're missing at that point.

So of course, we should look into that.

So you can open the file by, so when the document-based app opened your app, it opened the file by URL.

But it's not doing that state.

Like, it should use NSFileCoordinator and read using a file coordination request.

That is something apps using UI documents do for free, so when that happens, your file provider extension is called.

Most specifically, in your subclass, startProvidingItem at URL with completionHandler is going to get called.

When you [inaudible], you look into your disk storage.

You figure out the item is not there.

So you go in your server, NSURLSession most likely, and you download the file.

The download completes.

The file is moved into your disk storage.

Now, you can call the completionHandler that is going to grant the coordination to the document-based app.

And at that point, the file is opened.

Cool. So what happens if you want this a second time?

Because this time, the file is there.

But there is still going to be a file coordinator reader request issued.

And at that point, this is pretty cool.

Your file provider extension subclass is going to get called again.

So that's very powerful for you to track usage on your files.

But this time, the file is here, so what do you need to do?

Well, you just completionHandler, and you're done.

It's very powerful, but at the same time, it means that your file provider extension might be, might delay file loading.

So you better be fast when you implement startProvidingItem if the file is already local.

OK, so the coordination is granted, and you've seen this before.

The file is now opened.

Second part.

In the Files app, the metadata of your files are prominent.

And the way it works is that for each item that the files have displayed, your file provider is going to be called, and the specific method this time is called item for itemIdentifier, which returned an NSFileProviderItem.

That item should be [inaudible] from your database.

So you look at your database, you get an NSFileProviderItem, and you return it.

OK, that was simple, but I did not actually go into the detail of what is an NSFileProviderItem.

So as I told you a bit earlier, it's a protocol.

And with most protocol, you have required properties as well as optional properties.

So let's go into the detail of the required properties that your object conforming to NSFileProviderItem must implement.

So the first properties are usually the itemIdentifier, so that's a simple string.

Then, you have the typeIdentifier, and that is the UTI, indicating whether the file is a PDF, an image, or a folder.

And last but not least, you have the filename, which is pretty prominent in [inaudible] UI.

So now that we've seen the required properties, that's all you really need to implement, we can move on to the optional properties that we strongly encourage you to also implement.

For instance, in this particular screenshot here, there is the shared status that is displayed just below the filename.

That shared status is displayed onscreen if you set the isShared property.

And to go along with the shared status, you have the ownerNameComponents that you can specify.

And now, you can see on my document slide, "Shared by Amaury," which is pretty good to know.

Then, you have additional states such as isDownloaded that lets, display a small down arrow isUploading and uploadError.

So the uploadError is interesting because if your user doesn't have enough [inaudible] to actually upload the file, you probably won't position that error.

OK, there are a lot of additional properties.

Unfortunately, we don't have time to go through all of them.

So we're just going to move on, but I encourage you to read our documentation, which is pretty good on the subject, as well as our header files.

Well, so we've seen the metadata, but there is one last piece of metadata we did not talk about, and it's a fairly big one, so it used a different pipeline.

It's the thumbnail.

So when the Files app displays your file, especially if the file is not local in that case, there is no way we can generate a thumbnail out of that file what we are going to do is to ask your file provider extension to fetch the thumbnails for a list of files.

In that case, you can go on your server, ask it to download the thumbnail associated to the file.

When the download completes, you hand back the thumbnail data.

And at this point, we just display the thumbnail.

So I'm going to go into detail of how that fetch thumbnail function works because it's a bit more involved than the other one.

So first, what you have onscreen if your file provider extension subclass.

And so we did, it's a file provider extension subclass called MyFileProviderExtension.

And there, you want to override the fetchThumbnail function.

It takes a list of item identifiers.

Why a list?

Because we want to do batching for performance reasons so that we don't [inaudible] the server for every thumbnail.

Then, you have a requestedSize, a perThumbnailCompletionHandler, and then an overall completionHandler.

The first thing you want to do is to actually create a progress object.

The reason why is not so much for reporting, but more for consideration purposes so that if the thumbnail request is canceled because the user dismissed the Files app, then you would be aware and you would be able and you must, actually cancel your thumbnail request.

So now, imagine you download the files from your server using a download task.

For each thumbnail, you get the file UI that you would have downloaded and you map it.

So be sure to not allocate memory there.

So I encourage you to use the alwaysMapped option.

Finally, you call your perThumbnailCompletionHandler, and that's not the real finally, the real finally is this one.

You actually call the overall completionHandler.

So remember one thing: You must call it in case of success, failure, cancellation always.

And now, you can return the progress object so that the system knows how to cancel your thumbnail request.

Boom, well done.

We provided an item, so we're happy.

But now, let's move on to the next topic, which is enumerating items.

So for that, enumerating items means actually providing data to the screenshots or to the Files app that I have on screenshot beside me.

And we are going to see how to paginate items, sync changes, render push notification, and then signal changes.

So enumerating item works this way.

The Files app starts up, requests a page of item.

Your file provider extension return the first page.

It's all over.

There's still some room onscreen, so it's going to ask for another page.

And so on and so forth until it does everything.

The question you might ask is, why do we paginate?

What's the sense of all this?

So what you have to know is that your extension is granted a tiny fraction of the memory your app is granted.

If your app is in blue there, your extension is in gray.

You can't even see it onscreen.

What you also have to know is that above this limit of, if you allocate more memory than what you have available, your extension is terminated.

So we strongly encourage you and this is what pagination is all about to avoid peak in your memory allocation.

I just want to go through some other tips that we found useful and that you might also find useful.

Avoid using all data task.

Instead, prefer the download and upload task because they manipulate files, so they don't require memory allocation.

Use dispatch queues with an autorelease frequency of always.

Otherwise, you can get memory peak due to too many allocation in your autorelease pool.

And finally, as always, if you have a long running while or for loop, be sure to [inaudible] to autorelease [inaudible].

So in any case, we don't have time to go into more details, so check out this super session from a while back, and there are some other you can probably check out online.

OK, let's go back to enumerating items.

So on your Files app, you've opened a folder.

And it's going to call your file provider extension subclass and ask you to provide an enumerator for a given item identifier.

On waving [phonetic], you will locate your NSFileProvider enumerator and return it to us.

The Files app is going to store that enumerator.

And the next thing it's going to do is to ask you to provide the initial page of item.

You get a callback on your enumerator this time, which is called enumerateItems for observer, startingAt page.

You figure out the items in your first page.

You return them.

And you say, you're done enumerating.

There is my second page.

Well done.

We've enumerated a page.

If the file is [inaudible] to have more items, it will continue the, it will ask you for more pages using the exact same mechanism.

Let's go through the different methods we've seen.

So first method is in your subclass of NSFileProviderExtension.

It's called enumerator forContainer itemIdentifier, and it returns an object conforming to NSFileProviderEnumerator.

The itemIdentifier can be one of workingSet if we're enumerating the Recents, as Jean-Gabriel was describing earlier, or the rootContainer.

Finally, it can also be any of your folder item identifiers that you've previously returned.

The other function that we saw is this time on NSFileProviderEnumerator.

It's enumerateItems for observer, startingAt page.

The observer is a system, as we saw earlier, is a system object presenting, receiving your items and the next page.

The page is a simple data blob.

It has a type, but it's a simple data blob that you're free to decide what's, and you're free to decide what's inside, as long as it doesn't go below 500 bytes.

What you generally put there is like a page index 1, 2, 3, 4, 5 or an item of sets [inaudible] et cetera.

For the first page, it can be one of any initial page sorted by date or sorted by name, depending on what the UI is displaying.

OK, so let's go back to this slide that you've seen before.

One crucial thing we've just added is the ability to enumerate folders and the working set.

So we have two kinds of enumerator, of enumerators, as you can see onscreen.

And I just want to go back or actually, highlight the differences between the two.

So for folder enumerator, what you typically do is enumerate the cloud directly.

That's it.

You can do caching, however, if you want.

It's up to you.

You decide.

However, for the working set, it's very different.

You need to enumerate your own local copy of the working set.

So not the cloud.

Just your working set.

The main reason for that is that your working set must work offline.

The item is the working set, in the working set must be available offline and work offline.

I just want to reiterate on this because this is something that is not obvious.

So I'll say it again.

The working set should never read the cloud.

The folder enumerator, it's OK.

And the working set should always read the database, and the folders, it's OK if you want to cache them.

It's fine.

I hope it's clear.

Now, let's move on to the next topic, which is syncing changes.

So for that, on your cloud server, you are probably doing it anyway, but you need to assign version numbers to your items.

So here are some version numbers on my [inaudible] cloud.

The maximum known version number is what we call the sync anchor.

So here it's 14.

On changes, you need to bump the file version number.

So here, for instance, 15.

The count sync anchor oh, I went a bit fast, but it's OK.

I did a new file, which is, which gave the version number 16.

And this time, we bumped the sync anchor to 16.

And remember, it was 13 at the beginning, and then I went a bit fast, but you can see that now.

Then, if you, if we go back to the Files app in your file provider extension enumerator, what is going to happen and I lied to you earlier.

Before enumerating, what we are going to do is to ask your enumerator to provide its current sync anchor.

So that's before enumerating the pages.

So this [inaudible] you just have your sync anchor.

You return it back to the Files app, and the Files app is going to store it.

After that, we'll just fetch the pages as we did before, and we're in sync.

When a new file is added on your server, you add a new file here in green, you bump your sync anchor, it becomes 14, and you emit a push.

The Files app receives the push.

Your enumerator is called with this new function, enumerateChanges.

So before, it was enumerateItem, and now, it's enumerateChanges.

And it's passing you the previous sync anchor it has.

So here it's the current version, it's the version 13, so it's passing you 13.

When your enumerator gets that callback, it fetches the new item, which is here, 14, the item with the change number 14.

It sent it back to the observer, and then it called finishEnumerating up to the new version here.

This is 14.

And you don't have anymore change coming, so you just say, notComing false.

OK, and now we're ready to display the update.

Oh, we forgot one thing, which is the Files app [inaudible] the new sync anchor.

So I went very fast on push notification, and it's actually interesting because for this time, the push notification doesn't go to your file provider extension.

It goes to the file provider daemon, actually.

And so when receiving your push, the file provider daemon is going to initiate the enumerateChanges code on your enumerator, and at that point, you can return the item.

Another [inaudible] what you have to know for push is and I will go briefly into this your PKPushType must be a PKPushType file provider.

The topic name is your bundle-identifier plus a suffix, pushkit.fileprovider.

And your payload must include a container identifier, which refers to one of the possible enumerator.

For more on push notification, check out this session, Introduction to Notification, from last year.

So we've seen pushes, but you might ask, what if I have some changes and I want to tell the system about it, but I don't have a push for it?

In that case, you would want to code the file provider daemon and just say, oh, I have a change.

Well, we have a function just for that, which is called, NSFileProvider signalChanges forContainer.

When receiving such a code, the file provider daemon is going to enumerate changes just like it would do for pushes.

So let me go back to that method.

The signalChanges method, when should you call it?

So first, you can tell, you can call it for the initial set of files.

Imagine you just installed your app and you already have some files in your file provider extension that you want the system to know about.

Then, in case of changing account for instance, the user is logging out, logging in to a new account you would want to tell the system to reload after this account change because usually there were no pushes.

More generally, you would want to call that for any change without a push, including the one made locally.

So let me go back to one thing, which is earlier we saw that your file provider needs to implement [inaudible] providing [inaudible].

If the file is not local at that point, you need to tell the system that, well, this file is not downloading.

If it is, what you should do is you should signal for changes in the working set if the item is in the working set.

Same thing if you have an enumerator running for the parent, you should call signalChanges on the parentItemIdentifier.

Well, so under the hood, signal changes can be called from both your app and your file provider extension, so it's very convenient.

You must call signalChanges even for system-initiated changes.

This is exactly what I was describing earlier.

You could say the system could infer that the file has changed.

No, we don't do that.

We let you explicitly tell us that you know about this change.

But don't worry about it.

signalChanges is very, very cheap.

So now, we've seen all these three topics on screen: creating the project, providing an item, and enumerating items.

So now, it's time for Johannes to come on stage and cover the last topics.

[ Applause ]

Thank you, Pierre.

So we've seen how to get your items into the Files app or into the document browser.

But of course, that is only part of what you are interested in doing or what your users are interested in doing.

An equally important part is that your users are able to modify files.

And to modify files, there are various possibilities how you can modify files in the Files app and using other technologies.

So in the Files app, as an example, bring up this menu controller here as a user, and this menu controller gives the users a variety of options to modify files or to perform actions on files.

One of these actions, for example, is the Info option here, and that simply brings up a nice Info panel.

And that is backed by information that you're already providing.

That's the item method that you're already implementing.

Other options are, for example, the Share action here, and that is backed by a well-known old friend, the UIActivityViewController.

And of course, you're able to simply add activities using activity plug-ins, and that's another way for you to expose actions on these items.

But then, there are other possible options that are not backed by any existing system UI so far.

For example, renaming items.

Renaming items brings up a nice UI in the Files app, but we're not so far exposing a way to actually tell you about the rename.

And that is what this part of the session is about.

To back a rename or something like that, we expose a wide variety of options, and these options are moving files around, creating files for example, if you, if the user hits the little + button, that shows up in the document browser view controller, or if the user drag and drops a file into the Files app or into the document browser in another application.

Last but not least, modifying attributes on files.

So if the user performs a tagging operation or looks at a file and thus wants to modify the last use date of the file.

And all of these are actions that are backed by, that back user operations.

And you are, your extension is implementing these.

All right, so that is awfully abstract, so let's have a look at one of these actions and how you implement it.

All right, and as our example, we're going to use the Import Document action.

But all of the actions are actually implemented in a very similar way.

The Import Document action is a nice one because it deals with a new file, but most of these actions are kind of similar.

They have slightly different parameters and, of course, have slightly different semantics, but everything I'm going to tell you in this session is going to count for pretty much all of the actions.

So the Import Document action works this way.

The system gives you an existing file URL, so that's a file on disk, and this file is any location that the system gives you, it's possibly in the Temp folder or wherever the application that, for example, performs a create operation stores its templates, but in any case, this file is basically for you to use.

You can go and move this file into your file provider storage.

So this is, this file is now under your control.

It's in your container.

At this point, you need to schedule a background upload so that this file from your container actually gets uploaded into your cloud.

And last but not least, you call the completion handler.

This is very important, and this is a very important part of this.

We just scheduled a background upload, but we did not wait for the background upload to finish until we call the completion handler.

So the file is actually being uploaded in the background, meaning that the user does not have to wait for this file to be actually fully uploaded until you tell the system, hey, I got this.

I've got this file under my control.

I'll scheduled the upload.

This file will be in the cloud at some point.

So the user doesn't have to wait for this upload of the file that can be potentially many megabytes.

So that's how the general flow of this looks.

Let's have a look at how the data flow of this operation looks.

So as I said, the initial impetus for this operation is that the user created, or pasted, or dropped a file into the, into a container in your file provider.

And at that point, the Import Document action on your file provider extension subclass gets called.

You make a database entry.

You make sure that the file is getting uploaded.

So you schedule the upload.

Now, we're done, right?

We can call the completionHandler.

But as Pierre has told, the system does not automatically infer that this file basically has changed.

So one important bit here is that you at this point signal us to tell us, hey, there are changes on this container.

We need to re-enumerate this container.

This is important so that the user gets immediate feedback.

So the next step is actually that you tell us there are changes here.

And at that point, we simply make a note.

We say, OK, for this container and keep in mind, the user is probably looking at this container, right?

They just hit the little + button there.

Of course, they hit the + button in the container that they're looking at, so they want to see these changes.

So you told the system, changes are pending.

Now, you tell the system, call the completionHandler.

We're done with this operation.

The system gets unblocked.

And at this point, the system can go and call, turn around, and enumerate changes.

You tell us, sweet, we just added this item, and it's now uploading, probably.

And you return the changed item to us as part of a change set.

And we can update our UI and show a little uploading error in the corner of the item.

So the user gets immediate feedback that their data is safe in your cloud.

So let me reiterate that.

Operations are expected to finish immediately, and thus, you need to defer long-running tasks, such as uploads, to the background using NSURLSession.

So now, we've got this item uploading, and that's great, but, of course, at some point, this upload is going to finish and we want to tell the user about the new status of the file.

So let's go into that.

The way that works is that NSURLSession at some point will notice, hey, this upload succeeded.

Sweet, the item is in the cloud.

And the way this, the way NSURLSession tells you about that is that it calls a callback, an NSURLSession background callback, on your parent application.

Your parent application at this point can go and update the database entry.

It sets a little flag in your database that says, this file is uploaded.

It's no longer [inaudible] on disk.

Our upload succeeded.

We don't have to retry this upload.

And we should inform the user that this upload is finished.

So as Pierre mentioned, the signalChanges method can be called both by your extension and the parent application, and this is why.

Now, you tell us there are changes on this container.

And at this point, the same old dance happens.

The system notes that changes are pending, it re-enumerates your, the, your working set, and at this point, you can return in, from your file provider extension that this item is properly uploaded using the isUploaded key on the NSFileProviderItem protocol.

Now, sometimes uploads fail.

For example, your server might tell us, hey, this user is out of quota.

And in this situation, the same dance happens, basically.

So NSURLSession D is going to tell your parent application, hey, this upload failed.

That's too bad.

You're going to have to handle this somehow.

So again, we make a database entry.

We signal the system.

The system notes that changes are pending.

The system re-enumerates.

And we can return the updated item.

Now, this updated item should reflect the fact that this item is now in an error state, so to turn an update error, an upload error that is reflecting what actually here.

All right, let's get into a bit more detail.

As I said, you mark the item as being in an error status by using the upload error property on the item properties.

You signal re-enumeration so that the user can see that this item's in an error state.

And then, how you actually continue in this error state depends completely on the kind of error that you're looking at.

So some, not all errors are the same thing.

There's the possibility that you're running into an intermittent error.

For example, the user was, I don't know, camping out in the jungle for two weeks, and NSURLSession D just gave up because there was no Wi-Fi, no wireless connection.

In this situation, the easiest thing to do is tell the user, hey, your data wasn't uploaded, but we're on it.

We re-enqueue the, your NSURLSession task.

We still mark this item as being in an error state, as not being up to date in the cloud, but we simply retry.

There's nothing that the user has to do.

In the error description, you can possibly tell the user, hey, maybe go online or something like that, but that's pretty much it.

There's also the possibility that you're running into a persistent error, and that is different.

In a persistent error case, like, for example, the user's out of quota, what you need to do is tell the user, hey, you're out of quota.

You got to go and buy more quota on our web page.

And in that situation, basically what you give us is a, an error that suggests a recovery option.

Now, last but not least, there's the possibility for authentication errors, and these are kind of special.

The user has to do something specific to your application.

For example, re-authenticate.

Well, that's actually the only possibility in that situation, probably.

And for that possibility, a custom action will be called by the system.

So what's a custom action?

Well, conveniently, that's our next section, and let's have a look at, back at this menu view controller here, menu controller here.

We've seen these system-provided actions here, and the way these work is that the user just taps them and is backed by an action that is internally backed by your provider.

But we also offer another opportunity for you to customize this, which is to introduce a custom action in this situation.

So that's a action, and this one here is called Custom on the slide, but it can be whatever name you want.

And the way this works is that you expose an operation, a UI operation, that is scoped to your file provider extension.

You provide an interface for this operation that is backed by a new type of UI extension.

And this is a separate process that's running on the system, and it's backed by a subclass of FPUIActionExtension ViewController.

So how do we do that?

Well, first of all, we list the possible number of actions in the info.plist of our extension, and this is really easy.

We have an action name, we have an identifier, and we have an activation rule.

And this activation rule is used to figure out which of these actions applies to which items.

So in this example on the slide, we're simply using a true predicate, which means that this action is, that every item in your provider's eligible for this specific action.

But you can basically go wild on this.

You can expose different keys on your items and use these for matching in your activation rule.

So how does this work?

Well, the user long presses on an item in the Files app or in the document browser, and at this point, we consult the index, not your extension.

We don't even bring that up.

We consult the index of the item that you previously returned to us.

We look at the attributes and we match this predicate against the attributes.

At this point, we know whether this predicate returns a yes or a no, and we can simply display the action if it returns a yes.

OK, so now, the user sees this action, and probably they tap on it.

So the user taps on the action.

What happens now?

Well, we bring up your UI extension, and your UI extension gets a call to its prepareForAction method.

What this gets, what this does is it, we hand it a set of items plus the actionIdentifier that the user actually chose, and you can go and present whatever UI you have for this action.

You return from this method.

We present your view controller.

It slides up nicely in the Files app.

And that's it.

At this point, you know which items you're performing these actions on.

For example, and you can do whatever is necessary to perform these actions.

And these actions are completely custom, so, of course, it's completely up to you what is happening here.

At some point, the user will be done with this action.

Maybe you're implementing a Done button.

Maybe this is, there's some natural end to this action.

But at any rate, you call the completeRequest method on the extension context, and the system simply dismisses your UI.

But that's just custom actions.

As I promised earlier, there's a special case for authentication actions, so look at that.

Authentication actions are called exactly the same way as custom UI actions are called, except they're called in a special case, and that's when you to us return an authentication error.

So if you return an authentication error, what happens is that we call the prepareForAuthentication method on your FPUI extension, ActionExtensionViewController.

And everything from there on flows just the same way.

You call, you can call, you can display whatever UI is necessary for authenticating.

At some point, the user has authenticated.

You can call the dismiss method, the view controller dismisses, and you're now authenticated, and we will simply retry.

So those are actions, and those are very nice, but all said and done, they are only showing up in the, either the document browser view controller or the Files app.

And that is great.

That is a great way for you to expose something to your user, but sometimes you need something more, and for that, we have this concept of Services.

What's a Service?

Well, as I said, there's caveats for the custom actions.

But sometimes you need programmatic access to specific files.

And if you're currently publishing an SDK, for example, this is where you export, expose, where you can expose the same functionality that your SDK is currently exposing.

Services are a way for you to expose functionality directly on an item, so let's have a look at how that goes.

First, you need to define what your service is.

And at the base level, a service is simply a name that's an identifier that you give the service plus an Objective-C protocol.

And this is basically any old Objective-C protocol.

There's some caveats here that we'll go into real soon.

The protocol must be known to you and to the developer that's using it.

So both of you need to determine what the protocol is.

And this is because the developer using this is going to perform method calls on an object that is exposed by you, effectively.

So if the signatures don't match, nothing will happen.

This will just result in an error.

And when I say they're performing method calls, the way this works is actually that this is implemented using NSXPC, which is a great technology for interprocess communication.

And that means that the usual XPC rules apply.

So all the parameters that are passed through this protocol have to be secure codable.

And of course, the classes have to be available in both your extension and the app that's calling this.

And you cannot directly return any objects from this protocol.

So you have to return, if you want to return something, you have to call back via completion block.

Now, the rules for this are somewhat interesting, and there's, was a session in 2012 that goes into way more detail than I can go, possibly go into here today.

So I encourage you to have a look at that session.

All right, let's quickly go through the data flow that is happening here because that is, that warrants having a look at.

Your third-party application goes and basically inquires on an URL, what are the services that are exposed on this URL?

And at this point, your extension gets a call to answer, hey, what are the supported services on this item?

It returns an array of service sources, and a service source is not a service itself.

It's an object that can create a service, effectively, that can create an XPC listener endpoint.

We return these services as services to the application.

So on the service object, the application can go and then create a proxy object.

Let's have a look at that, how that works.

So the third-party application uses that service after it's looked at which services are available.

Of course, they can be any number of services, right?

So it looks at the identifiers of these services, figures out which one it wants to use, and creates a messenger.

At this point, we are asking your extension on the service source to make a listener endpoint.

So it returned that.

A ListenerEndpoint, that's an existing NSXPC class, although it's currently only exposed on macOS.

We're now bringing it to iOS.

At this point, the third-party application can grab the remote proxy on this messenger, and that calls on your listener delegate the shouldAcceptConnection callback.

At this point, you can call, you can configure this connection and return this connection to the system.

So in this case, for example, you provide a proxy object that is being exported by our extension.

And from there on out, we can go and call whatever methods are defined in our protocol on this remote proxy object in our third-party application.

And all of these calls will be translated into calls in our extension.

This is a, an extremely powerful mechanism for you to expose basically any old possible SDK operations on your extension.

As a, as an aside, we don't expect everyone to implement this, and it's perfectly fine if your first version does not use this mechanism at all.

This is a very advanced topic, so if you only, if all you implement is enumeration and the usual file provider actions, we're already super happy.

All right.

Let's summarize what we've seen today.

We've seen what a file provider is, and how we can use a file provider to back the enumerations in the Files app, and how to use it to show files in the document browser.

We've seen how you use enumerations to expose items to both of these controls and how you can enumerate the working set, how you can enumerate changes to the working set, and how you can push changes to the working set.

And finally, we've seen how you can implement actions to modify stuff in your hierarchy and how you can then go even further, and customize these actions, and even implement services that are completely custom to your provider, and expose those to third-party apps.

For more information, we have a page on the and we have a list of related sessions that go into way more detail than we can possibly do here on some of the technologies used.

And with that, I hope you are having a great conference.

Thank you very much.

[ Applause ]

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US