WWDC2017 Session 243

Transcript

[ 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
[rewrote] their document browser
to be that new piece of system
UI; and all the third-party
[adopters] 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 [enough data] 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 [remote] 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 [hand over to Johannes] who is
going to go through you, with
you and [to see how] 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 [role] 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 shown on screen behind me.
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 you, the system does it
for you, but you return it to
the system which returns it to the
document-based app, and you're
done.
At that point, the
document-based app opened a file
by URL.
Under the hood, 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.
More specifically, in your
subclass, startProvidingItem at
URL with completionHandler is
going to get called.
When you receive such a call, you look
into your disk storage.
You figure out that the item is not
there.
So you go in your server, through
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 itemForItemIdentifier,
which returns an
NSFileProviderItem.
That item should be constructed
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 protocols, 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
obviously 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 the 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 quota
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 hit 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
autoreleasepool frequency of always.
Otherwise, you can get memory
peak due to too many allocation
in your autoreleasepool.
And finally, as always, if you
have a long running while or for
loop, be sure to add an
autoreleasepool keyword.
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.
Hand waving: 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.
We're done.
We've enumerated a page.
If the Files app wants to
have more items, it will
continue to ask
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 inside, and you're free
to decide what's inside, as long
as it doesn't go above 500
bytes.
What you generally put there is
like a page index -- 1, 2, 3, 4,
5 -- or an item of sets
100, 200, 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 items in the 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 is trivial, 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,
moreComing false.
OK, and now we're ready to
display the update.
Oh, we forgot one thing, which
is the Files app actually remembers 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.
Under the hood, 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 startProviding
itemAtURL.
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 dirty 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 developer.apple.com
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 ]