(enlightening music)
- [Announcer] Stanford University.
- [Lecturer] All righty then, Lecture 13
of Stanford CS193p Spring of 2020.
Today, our topic is persistence.
That is to say, storing stuff that lives
between launches of your application.
Now, we've actually seen quite
a bit of this stuff already.
And we're gonna go into some more detail
on the last two down there,
CloudKit and file system
but let's do a quick review
of what we've learned about persistence.
We know about UserDefaults.
It's simple, quite limited,
only lets you store
these property lists.
It's small, it only stores
a little bit of data.
It's also pre-Swift, just
kind of a clunky API.
But it's really good for demos,
and that's why we've used
it so much this quarter.
And we learned about Codable
and JSON earlier as well.
Great way to take a custom struct
that we've designed and turn it
into nice interoperable format,
either that we would
send over the internet,
or maybe that we would store on disk.
Now, for those of you who looked into
the Enroute code that I wrote
that got the data from FlightAware
would see that that data from FlightAware
comes as JSON, and I just use Codable
to turn it into a local struct.
So that's another use of Codable
is receiving data from
people in JSON format.
This UIDocument is something
that is part of UIKit.
So we're not really gonna
talk about it in this class
since this is a SwiftUI class.
However, if you have an
application like EmojiArt
that really has what the user perceives
to be a document that they're creating,
you almost certainly would wanna use
this UIDocument infrastructure.
This is not gonna be something
that you're gonna do
for your final project.
It's not just that it's
a really advanced API,
as much as it requires UIkit integration,
and some concepts there that we just
don't have time to cover, bottom line,
but I'm mentioning it just so you know
it's out there because when you,
if you went out in the real world,
and you created some kind
of app that has a document,
you wanna know about UIDocuments.
And there's Core Data, of course,
powerful, object-oriented,
it's incredibly great SwiftUI integration
through that FetchedResults stuff.
And this is really the go-to place,
when we want to store data in an iOS app,
we're gonna Core Data.
For any significant amount of data,
we're gonna do this.
We're not gonna put it in UserDefaults
unless we're doing a demo.
We're gonna use Core
Data, and you saw why,
it's really very capable database system.
CloudKit is something I'm
gonna talk about today.
And it's a way to store data
in on the network in iCloud.
And you can see there's some
huge advantages of doing that.
When you put stuff out on the network,
it means that now, the
user's gonna see all
that data on all of their devices,
instead of just on the device they created
the EmojiArt thing or the
theme in their Memorize.
So it's really, really powerful thing
to be able to do, store
things on the network.
CloudKit has some nice features
that are similar to what you already know,
for example, it has its
own little UserDefaults
like thing, where you
can store key-value pairs
on the network that are shared
on all devices for that user.
And it also can play really
nicely with Core Data.
I know if you remember when
we did the new project,
and we clicked the Use
Core Data button there,
there was another one that
said, "Oh and Use CloudKit too."
And now, we'll make it so
that your Core Data databases
get replicated using CloudKit
on all of the user's devices.
So anything they put in Core
Data, they see everywhere.
That's a powerful combo right there.
And CloudKit also has mechanism
for storing documents out there.
So there's some integration
with UIDocument and all that,
to make a document store,
so CloudKit's really,
iCloud in general,
really awesome mechanism
for saving things on the network
so the user can see them
on all of their devices.
Now, we're gonna go over the basics
of CloudKit today via slide.
I just wanna give you a
feel for what it's like.
This is an API you might wanna try
and use for your final project.
It's a little ambitious,
and I'm gonna show you
ways you can cut corners
just a little bit for your final project
as soon as you introduce yourself to this.
This is an introductory course after all.
But I think some of you might find,
"Wow, I really need a way to store stuff
"that would work on all
the user's devices."
And so, CloudKit might be a nice
your-choice API from the rubric.
Then, the next stuff to talk about
after CloudKit is the file system
essentially accessing
anything in the file system,
which we do via URL and Data,
which you already know
about those structs,
and also a new struct called FileManager.
I'll talk about how these things work,
and then, I'm actually
gonna demo on this one,
where we're gonna make
our EmojiArtDocuments
be stored in the file system.
So CloudKit, what is it?
It's a database in the cloud.
Now, it's a simple to use database,
has basic database operation,
not as fully featured as Core Data.
This is not Core Data over
the network, all right.
Now, one of the most important things
to understand about
doing CloudKit database
is that it's asynchronous.
All of the important calls that do things
in CloudKit are asynchronous.
You provide them a closure,
it goes off and does it
on a background thread,
and when it's done, it
calls your closure back,
and said here's what happened.
That kind of programming,
asynchronous programming,
takes some getting used to.
You already saw it a little
bit so far in this course.
But CloudKit is intensively asynchronous.
By its very nature, it's going out
over the network, which the network
might be unavailable or it
might be slow, or whatever.
Demoing this is, because of that,
can be quite a big demo,
even bigger than Core Data demo.
So I'm not gonna do a demo this quarter.
If you want to go back
to spring of 2015-16,
this course was on iTunesU.
You can, I think it's
still there on iTunesU,
you can go watch it, and
see the CloudKit demo
that we did and most of
that is still applicable.
This, all this cloud stuff is
pre-SwiftUI, pre-Swift even.
And so, the stuff hasn't
changed that much since then,
you'll get the basic idea
if you wanna go back and watch that.
All right, so let's do
an overview of CloudKit.
I just wanna define some terms
that we use in CloudKit a lot,
so we understand what we're talking about.
The first term is Record Type.
So a Record Type is like
a class or a struct.
There's really no classes
or structs per se,
stored in CloudKit.
It's not like Core Data,
where we essentially
looks like objects to us,
but we do have these
things called Record Types,
and that's a kind of thing
that's in the database.
Then, the word "Fields"
is what we use for vars.
In Core Data, we call these attributes.
In CloudKit, we call them Fields.
This is the vars, the things that exist
in our Record Types that we store.
Then, there's the word Record.
Record means an instance of a Record Type,
so it's an actual one of
those things in the database,
so of course, you're
gonna be storing many,
many Records in your CloudKit database.
And that's where all your data is,
and the Records contain
values for all the Fields.
There's something called a Reference,
capital R, Reference, a CKReference.
You're gonna see CK in
front of all the things
that we do in CloudKit.
And that is a pointer to another Record,
doing relationships between Records,
Record Types, is not quite the same,
or as powerful if you're having Core Data,
where it's automatically
keeping a set of the objects
on the other side, and
it keeps it up to date,
and all that business.
This is not a full relational
database by any means here.
So references between
objects still make sense
but you have to do them
with this CKReference,
this Reference to the other object,
and I'll show you that, what
that looks like in code.
Then, there are terms like
Database, Zone and Container.
A Container is a collection of Databases.
A Database can have Zones inside of it.
This is just how we partition up the space
out in iCloud that we
want to store our data in,
and I'm gonna talk a little
bit briefly about this.
I'll talk mostly about
the top level databases
that we use, where you're gonna use one
of three databases, and you can see that.
Then, there's something called a Query.
A Query is a Database search.
This is where we're gonna
go out in the Database,
and try and find some Records,
some instances of some Record Types
that match some criteria.
And that's gonna look very familiar to you
when we get to that.
And finally, there is a Subscription.
So this is like a standing Query,
and we talked about standing queries
in Core Data, where we have
the SwiftUI FetchRequests
that return the these FetchedResults,
and that was a standing query,
it was always updating them.
Well, here's a standing query too
but it's a lot more complicated
to have a standing query on the network,
because you're talking about finding out
when something changes in iCloud,
and then, notifying your app, whoa,
this Query that you set
up to be always querying,
it changed but it changed on the network,
and you get notified, and the
way your app gets notified
is via something called
a push notification.
These are the little things
that come down to your phone
and tell you certain things have happened.
A lot of apps have these
push notifications.
And that's how this works.
Now, that's way beyond
the scope of this class
to talk about how push notifications work,
and how you would react to them.
This is not something I would expect you
to do on your final project is to have
a Subscription, a standing Query,
and handle the push notifications.
You have three weeks to do your project.
I don't want you spending
a whole week on it,
trying to do that so I would,
if you're gonna do CloudKit
for your final project,
I would not tackle Subscriptions.
One thing about using CloudKit
is it requires a little bit of enabling.
You're used to just, I don't
know, import Core Data,
and I can start using Core Data,
and that's true for Core Data,
but for CloudKit, you
actually need to turn it on.
And if you go to the Capabilities tab
in your Project Settings, you're gonna see
there's a bunch of
capabilities like Apple Pay,
and Game Center, and things like that,
that you have to turn on,
and that's because these things
are accessing servers
out there in the world,
so they need a little
bit of an authorization
and enablement, entitlements
we call these things
to make your app be able to do these.
But it's quite easy to do.
You just go in here to
this Capabilities tab,
and we see iCloud, it says
"OFF" when you get in there.
Just click it to "ON", and you're gonna
get some more UI here,
and you can see there's three different
services in CloudKit.
The first one there, key-value storage,
that's the UserDefaults like thing
I was telling you about,
and then, iCloud Documents right there.
That's for storing your
documents in iCloud.
I was telling you about that.
And what we're gonna
talk about is CloudKit,
which is this database, right,
the Database, the Records,
and Fields, and all that.
So you're gonna turn that on,
and then, you're gonna go down
to this button right below,
very important button, CloudKit Dashboard,
this CloudKit Dashboard is gonna let you
manage all your activity in CloudKit,
all your Record Types, all your Records,
see all your data queries,
everything is going to
be managed through this.
Let's click on this button,
or simulate clicking on this button,
and see what we get.
This is what you get,
you go to a certain
website on the internet,
and it probably doesn't look exactly
like this anymore, this like,
it says created May 15, 2016,
so this is kinda old,
but it's still same idea
what's going on up there,
which is that you are
looking at all the Records
and Record Types that you have,
and you're even marking them.
You see on the right there
which is Query, Search,
Sort, Query, Search.
You're marking which things
you wanna be able to query,
and which things you wanna be
able to sort on, et cetera.
You can add Record Types, and Fields,
and all that stuff here,
although you usually don't do that
because something very
interesting about CloudKit
when it comes to adding
Record Types and Fields,
which is that it has
dynamic schema creation.
So you kind of look at that dashboard,
and you think, "Oh,
that's kinda like the map
"we had with Core Data," and it kinda is.
It serves a lot of the same function.
However, in Core Data,
you have to do the map.
The map is how your app knows
what classes and vars to create for you,
so you can access the
data, where in CloudKit,
you don't have to do the map.
It can build the map on the fly.
If you create a Record
Type with a certain name,
it just creates that very
first time you ever do it,
boom, it creates it.
Now, it only does this dynamic creation
of the Record Types,
and Fields, and stuff,
while you're in development,
and eventually, when your app ships,
when you go to App Store,
the iTunes Connect, where you set up
to have your app be on the App Store,
you're gonna click a button says,
"Okay, I'm switching over to production
"mode on my CloudKit," and then,
it's not gonna let you do
this dynamic schema creation,
of course, right.
You do the dynamic schema creation
while you're in development
once you get that
to the Record Types, and
Fields, and stuff you want,
then, you go live.
Let's take a look briefly
at what the code looks
like when you're writing a CloudKit app.
The first thing you need is
a Database to put things in.
It's kind of like a ManagedObjectContext
but not really 'cause it's much,
much more lighter weight
concept, this Database.
But the most important thing
you're choosing between
is whether you want
a public Database, a shared Database,
or the private Database.
So the private Database is
the normal iCloud Database.
This is just the Database
where you put stuff
that is the user's, and the user sees it
on all their devices,
that's the normal Database.
The shared and public are
kind of interesting ones.
The public one, if you,
if a user puts stuff
in the public Database for themselves,
then, other users, if they
know that person's iCloud
email address or whatever,
they can actually look
their app, and go look, and see that data.
So it's really kind of
publicly posted data.
You can think of it as like your website,
making the stuff available to people,
and they can see it all they want.
Pretty rare to do this on iCloud,
but you could do it.
Essentially, a way to use iCloud
to publish information to the world.
And then, they're shared,
which is invitation-
only access to your private Database.
And the way this works is you end up
sending an email to people.
And in that email is a link,
and when they click on it,
their little iCloud shared Database
gets a view onto some
part of, not your entire,
private cloud Database of someone else.
So that way, the two
of you can share data,
and both sides can add objects,
or whatever depending on
the permissions granted.
Add objects and see each other's objects,
and again, it's only a little sub-part,
so a little sub-part of the Database,
but it's a kind of a
cool way to share things.
So once you have a Database,
then, you can start creating Records,
and here's how you do that.
To create a Record, you just say
CKRecord and the name of the Record,
and again you don't have to have gone
to the dashboards first.
You can just do this, and
if this is the first time
ever created a CKRecord "Tweet",
then now Tweets, or Record Type Tweet,
is just going to be created
in the Database for you.
And then, to set the values of Fields,
that kind of looks like
accessing a Dictionary,
you say tweet open square
bracket the name of the Field,
equals the value, and that is going
to again create that text
Field in the Tweet Record Type,
if that doesn't exist before,
and set its value to that.
And here, I'll create another one.
This is a CKRecord for a Twitter user.
So it's a different Record Type,
a different kind of thing.
I'm going back to my tweet
and setting its "tweeter" Field
to be a Reference to the tweeter.
So see here, I'm having to say CKReference
with the record of this
TwitterUser I created.
A little different again from Core Data.
You can't just set it directly,
you gotta do a CKReference thing,
and then, notice that action
that says things like basically
determines what happens
if the Tweet gets deleted.
Does the TwitterUser get deleted?
Things like that, those
kinda relationships.
So that's how we build
up our Record Types,
and our Records, and their Fields,
and the References to other objects.
And once we've kinda done that,
then, we wanna save it.
And this is where we're
gonna hit the network.
So this is an asynchronous function.
Just call save, we just
give it the Record,
the CKRecord you wanna save,
and it's going to go off on
the network in the background.
It might take a long time
if the network's down
or might fail if the
network's down eventually,
but if it can finally get through,
and write the information that you want,
this Tweet Record, then it's gonna call
this closure that you give it back.
And this closure that you
give it has two arguments.
One is the record that it
saved, if it was successful,
and the other one is the
error that was generated
if it was not.
So this is again pre-Swift,
in Swift, we'd probably
have an enum in here
with success and failure,
and associated value of
the Record for success,
and associated value of the error
but we don't have any of that,
it's all pre-Swift.
So one or the other of these two things,
the savedRecord or the
error, it can be nil.
The error is nil, then whoa, success,
you saved that thing and the savedRecord
will be the CKRecord you just saved.
But if the error is not nil,
then, you gotta start
looking at the error codes,
and figure out what went wrong.
And it's the network, so a
lot of things can go wrong.
There are 29 different, at
last count, CKErrorCodes.
Now, you don't have to
check every single one
'cause some of them can't
happen during a save,
some errors happen in other things.
But if you're really doing this for real
and shipping your app, you'd want to check
all the ones that could
reasonably happen here,
and decide what your app is gonna do
because you weren't able
to save this Record.
Here's a place again if you're
doing your final project.
I'm not gonna hold it against you
if you don't check all these error codes.
Maybe check error codes in one place
just to show me you understand,
that you gotta check error codes,
but otherwise, don't check them.
And then your app, your final project
basically would just fail miserably
in bad networking conditions,
that's okay, we can.
This intro class, you're just trying
to introduce yourself to
API, so like CloudKit.
You're not trying to be a master of it all
by the end of this quarter.
And that reduces the scope quite a bit
of doing CloudKit.
A lot of CloudKit is
handling these errors,
and the infrastructure for what you do
when you can't write things out to iCloud.
And so hopefully, reduces
the scope to something
where you could realistically do it
as a final project API.
Now, what about querying Records,
searching for Records?
And this oughta real look familiar to you.
Yes, it's NSPredicate,
the exact same NSPredicate from Core Data.
It has the same object now.
You can't have exactly
the same formats there,
because the CloudKit database is not quite
as powerful as Core Data.
So Core Data can do some things in there
that CloudKit can't but
basic stuff like equals,
and does this text contain
this search String,
for example, this is, let's say a Tweet
that we're searching on right here,
that is perfectly reasonable to do.
So you create the Predicate you want,
and then, you create
something called a CKQuery,
which just is the Record Type
that you're trying to find,
that you're searching for essentially,
and that Predicate.
So again, sounds similar to a FetchRequest
in Core Data, similar,
but not exactly the same,
but you get the idea.
And then, to execute the Query,
of course, you're gonna have
to go out onto the network,
so that's where this
function perform comes in.
You give it the query you want,
and then, you give it a closure,
and that closure will be called
when this thing either fails or succeeds.
And that closure has
two arguments as well.
One is an Optional Array of the Records
that it found by doing
that search for you,
or an Error.
And again, if it's an Error,
you have to deal with the fact
things that you were looking for
failed to even search for them.
And if it's not, then the
Records will not be nil,
it'll be an array of CKRecord,
and there you go, there's your CKRecords
that you're looking for.
So fairly straightforward to do searching
in CloudKit as well.
These standing Queries are just,
you take one of those CKQueries,
and you essentially
communicate it to the database,
and then, it's doing the
Query on the server side,
and then, whenever a
new thing gets created,
that changes the result of that Query,
it's sent to this push notification.
And if you do want to try and check out
how do I handle push notifications,
and what do I do, feel free
to try and jump on that.
Again, I think a little too much
for your final project here.
You can take a look at the
UserNotifications framework,
not just for push notifications,
but also for doing
local notifications too.
You might wanna check that out.
Those are kinda fun, that's just where
you kinda can set little calendar events
almost, things to go
off at a certain time,
when your app wants to
remind the user of something,
or do something in a timely
manner, or something.
All right, that's it for CloudKit.
That's the intro to CloudKit.
Let's talk a little bit
about the file system,
iOS devices, I don't
know if y'all are aware
but they are essentially Unix OSes
at the heart of them,
and they have a Unix file system there,
Unix-like file system.
And it starts at slash,
just like all your Unix file systems,
but of course, it has protections,
and you cannot see or write into most
of the Unix file system there.
And there, you can see
and write into though
is called your sandbox.
And your sandbox completely isolates you
from the rest of the world.
Your app can't see into
other app sandboxes,
you can't see outside your sandbox
to modify the system, of course,
and damage it in any way,
and this is really a
great idea, sandboxes.
I wish we had this on Windows, and Mac,
and normal operating systems.
Obviously, it makes a ton
of sense on the devices,
where you're installing
and uninstalling these apps
for each of them to
have their own sandbox.
Now, why do we do this sandbox?
Three main reasons here.
One is security, you don't want anyone
to reach into your sandbox
and damage your app
in some way by affecting its data
or doing something bad.
Also privacy, your app, obviously,
is collecting data from the user,
and you don't want other apps
to be able to see what that data is.
And really underrated but
powerful part of it is cleanup.
If someone deletes your
app from your device,
you want everything that
app has ever created
or touched to disappear.
And that's what happens
when you delete your app.
The sandbox is completely removed,
and so everything the
person's ever created.
Now, the sandbox has directories in it,
which are backed up to iCloud,
when the user has iCloud Backups on.
So if someone deleted
an app and they said,
"Oh no, my documents were in there,"
they can go back and get them.
By the way, when it comes
to storing documents,
a lot of times, we'd want to
store documents in iCloud.
That way if we deleted
an app off of my device,
it will still be on my other devices.
And if I reinstalled the app,
I would see them from iCloud.
That's why it's a really good idea
to store our documents in iCloud.
All right so what's in this sandbox?
Well, it's a bunch of directories,
special directories, kind of
specially named directories.
One of them is the Application directory.
This is where your executable is,
and your JPEGs, or
images, or anything else
that you drag into Xcode
to make your app work,
those live in there, and
they're read only there.
No, you're not allowed to
change your application,
add images to it or whatever,
you'd have to do that in other places.
Another really important directory
is the Documents directory.
This is where you store what
the user perceives as a document.
For example, your EmojiArtDocuments
would definitely be stored here.
If you didn't store them in iCloud,
you would definitely store them here,
but something like the emoji palette,
or maybe your Memorize themes even,
you probably would not store here.
The user does not like
perceive them as documents.
Though, those things will be stored
in the Application Support directory,
that's another directory.
Gets backed up by iCloud, it's permanent,
but the user doesn't see
them as documents in there,
kind of data that the user is creating
but not really document-oriented,
a subtle distinction there,
and it really doesn't matter too much
what how you do that unless you're using
the UIDocument stuff,
you're using UIDocument
that I mentioned earlier,
then, you definitely
want those documents
and only those documents
going in the Document structure.
You don't wanna put those in Applications
nor would you want to
put Application Support
stuff in the Document structure,
and confusing the user
about what's in there.
And there's a Caches directory.
This is temporary storage,
so this is storage that does not get
backed up into iCloud, and
if you deleted your sandbox,
it's gone forever.
So this would be things that you can
like easily get from the internet again,
images that you're just caching locally
for good performance.
But if you had to throw
away the Caches directory,
you could, and your app would still work
because it would just re-download
whatever needed from
that Caches directory.
If the iOS's disk started to get full,
it the iOS might start hunting around,
looking for Caches
directories and sandboxes
that are really big, that it can blast
to get space back too.
Rarely happens, most people's iOS devices
do not get full disks, but could happen.
And there's other directories,
which you can look in
the documentation for.
And let's talk about how we find out
what these directories
are inside of our app,
and we're gonna do that using something
called the FileManager.
The FileManager is an object, a struct,
that lets you, as its name implies,
manage the files in your file system,
including finding out where
these special directories are.
Now, there's two different
methods for doing this.
I'm giving you here a
little more complicated one.
There's a slightly simpler one
which is called URLs.
This one is called URL.
I'll use the simpler one in the demo,
so you'll get to see them both.
So this FileManager, how
do we use a FileManager?
Most of the time, if we're
working on the main queue,
which we are, most file operations
happen quick unless we're
writing a gigantic file.
So we can do them on the main queue.
They're not gonna block
like a network operation would.
We get this usually FileManager default,
FileManager.default,
which is the shared one.
Now, we probably wouldn't wanna use
that one off the main queue.
There, you'd want to create your
own instance of FileManager,
again, kind of an advanced topic.
So we just are gonna use the shared one
in the main queue and the function
that we're gonna call on this
shared.FileManager.default
thing is called url for
directory in domainMask,
and it's got these other things
appropriateFor and create.
So what are the four arguments
here to this URL thing?
The first one is which of
the special directories you want?
So this is an enum,
FileManager.SearchPathDirectory.
Go look in the documentation,
and you will see, I think there's 12
or 15 different special directories
inside your sandboxes, like
the Document directory,
Applications, Support, Caches,
all the things we talked about,
there is an enum value for each of those.
The second argument is
in domainMask, in iOS,
this is always .userDomainMask,
because on a Mac, we might
have the network domain,
a shared library we're looking at.
But it on an iOS device,
it's a personal device,
so it's always userDomainMask.
Don't worry about appropriateFor, create.
If you're writing to
the Documents directory
for the first time, for example,
you would want to create it,
so you probably are gonna put create true,
when you do this.
And that's gonna return for you,
the URL to these special directories.
So now, that you have the
URL to the special directory
in the sandbox, you can start creating
or looking for files in there.
Now, how do we do that?
We do that with the methods in URL.
So we got this base URL,
and we're gonna use methods in URL
like appendingPathComponent,
and appendingPathExtension to build a path
to whatever file we're looking for
inside these special
directories, simple as that.
You can also ask the URL, things like,
"Is this the URL of a file
"versus the URL of a
network thing or web page?"
And you can ask the URL,
"Give me values for certain
resources in this file,"
like its creation date,
or whether it's a directory or not,
or how big this file is.
And the API for this, again, pre-Swift,
not that weird, except for that
you give it the keys of what values
you want about this URL,
like its creation date,
or file size, or whatever,
and it gives you back a dictionary,
where those keys that you gave it
are the keys and the values are in Any.
And the reason value
has to be in Any there
because it could be a Date,
could be a file size,
would probably be an Int,
those kind of things.
So that's how you find out about URL.
So URL is an important part of interacting
with the file system,
just like FileManager is.
Another important struct is Data.
Data is how we actually
put the data out on the file system,
and you already know how to read data
from the internet using
Data's contentsOf URL.
And you can do the same
thing with a file URL,
so that's how you read
files off your disk,
out of your file system is with Data.
And oh, same thing with writing.
There's a write(to url: URL, ...),
and you just send it to a Data,
and it puts the contents
of itself into that URL,
in the file system.
Both of these have little
reading and writing options.
The writing ones are the
only ones that are very
interesting. For example,
there's write atomically.
So if you're writing a gigantic file,
and let's say the disk
fills up halfway through,
the file will now not
be in a corrupted state
halfway written, it'll just revert back
to the state it was before
you tried to do this write.
So that's an atomic transaction.
Like essentially, it
writes to a temporary file.
If that's successful, then, it'll move it
into the URL you're trying to write to.
FileManager can also potentially help you
understand what's going
on in the file system,
similar to what URL was doing.
And it also can do things like,
show me all the files that
are in this directory,
or move this file from
this URL to this one,
or copy this file, make
a copy of this URL.
And I'm gonna show, instead
of going through slides,
and showing you all the things
that a FileManager can do, first of all,
you can just look at the documentation,
but I'm actually gonna show you
in a demo in here in a moment.
One thing I wanna note about FileManager,
it has something called a delegate.
Delegate is just a var on it.
And that var is an object that you set,
and that object will be notified
when certain things are happening
in the FileManager.
As a FileManager goes around, moves files,
tries to do things, it's
going to talk to its delegate,
and ask you to do things.
Now, this delegate stuff,
fundamental to UIKit.
Everywhere in UIKit, there's delegates,
and we'll see that in our next lecture,
because we're gonna
start talking about UIKit
integration with SwiftUI.
But if you're looking at FileManager,
you're probably like, "What
is this delegate thing?
"Never seen it before."
And I don't think you need
a FileManager's delegate
to do most stuff in FileManager.
We're going to do our demo,
and never even set the delegate,
but I just don't want
you to be surprised by,
"Oh, what's this delegate thing?"
It's essentially just an object
that gets notified when
things are happening
during FileManager operations.
All right, so a demo is
worth a thousand words.
As we always say, thousands of words.
So let's check it out.
What we're gonna do in our demo today
is store our EmojiArtDocuments
into the file system
instead of UserDefaults, which was silly,
that was demo-ware.
Storing in the file system
makes a lot more sense.
Storing them in iCloud would
make even more sense than that.
But we will store them in there,
and this is gonna be able to show us
all parts of accessing the file system
because we're gonna have to get a URL
to our Documents directory,
so we're gonna be using URL to do that,
and FileManager to do that.
Then, we're gonna write
our data out, of course,
using the Data object.
And then, I'm also going to
make my DocumentChooser work,
so removing documents,
adding new documents,
are going to have to work.
And so, we'll have to
be using the FileManager
to do some file system operations
to make that stuff work as well.
So let's dive into that demo right now.
So the goal of this demo is to make
EmojiArtDocumentStore stop storing itself,
right here, and EmojiArtDocuments,
over here, in UserDefaults.
Instead we want them to store
in the Unix file system
that underlies iOS.
So it's really two things we have to fix.
We have to get documents to
be storing in the file system,
and we have to get the DocumentStore
to be looking in the file system
instead of looking in UserDefaults.
Let's start with the Document here,
and I'm gonna add this
capability to the Store
and to the Document to
work in the file system,
and leave this UserDefaults capability,
in case someone wants that instead.
There's no reason to
break what we had before.
So creating an EmojiArtDocument here
that stores itself in the file system,
it's just gonna be for me,
a matter of a different init.
So this init takes a UUID,
and which it uses to store in default.
I'm gonna have my init,
instead of that, take a URL.
And this is gonna be the URL
to use to read the Document
from the file system initially,
and then, also to save the Document
anytime we want to autosave,
we're gonna use this URL for that.
We're still gonna need to set our id
to be some UUID because this id
is part of our Identifiable, right?
So we still need to do that.
But in here, we're going to also grab
a hold of this URL and
put it in a little var.
We're gonna use this URL
to load this thing up
and to autosave, so let's start
with the loading of it up.
It's pretty straightforward here actually.
We already know how to load
up an EmojiArt from a URL,
because we know how to
get a Data from the URL,
and we know how to load
an EmojiArt from Data.
So let's just do EmojiArt here,
and we're gonna pass it some JSON,
which is trying to get
the contents of this URL
from the file system,
and if we can't do that,
by the way, let's just
do a blank EmojiArt,
because maybe, they gave us the URL
that they want us to save to
but there's no such file
currently in existence,
and so we'll create a blank file for them.
So it's the same data contentsOf
that we were using when we fetched
things over the internet,
we're just using it now with a file URL.
Oh, we still want to fetch the background
image data here from that,
whatever that's coming from.
And we still want to do
autosaveCancellable up here.
But of course, we're not going
to autosave into UserDefaults.
This time, we're gonna do
our autosaveCancellable
to sync off of our EmojiArt.
For this autosave, we want to essentially
save our EmojiArt, have a
little function to do that,
little private func save, which
takes an EmojiArt to save.
And all this is going
to do is do the kind of
the inverse of what we did
right here with this Data.
We're going to say, if our URL is not nil,
in other words, we're the
kind of EmojiArtDocument
that saves to the file system,
then, we're gonna try to
have our EmojiArt's json
write itself to our URL.
One liner here to write ourselves out.
Now, one other thing
I'm thinking I might do
is here, I'm autosaving
every time it changes.
What if somebody changes this URL on me?
So, my Document was
writing out to some URL,
and autosaving, and then,
they change this URL.
Well, the next time I autosave,
that's okay, it'll write to this URL.
But I'm gonna be a little
more immediate than that.
I'm gonna put a didSet in here,
which does save of my own EmojiArt,
just to immediately save to a new URL
if somebody sets it on me.
Probably not critical but I think
if someone sets a new URL on my document,
they probably want it to pretty
quickly write that thing out,
they don't want to rely on the user
having to add an emoji or something
to cause an autosave done.
And this is it, it's
really all that's required
to get our EmojiArt to write itself
into a URL, quite straightforward.
Now, the other piece of this whole puzzle
is back in our store right here,
because right now, our
store keeps the names
of all the documents in
a little Dictionary here,
which is reading and
writing from UserDefaults,
and we want these names to
come out of the file system.
So we wanted to look into a
URL of a directory somewhere,
look at all the files that are in there,
and use those as the
names of all the documents
that are in there.
So this also is something we can do
without breaking our existing init named,
where it's going and
looking in UserDefaults.
For this, we can just do a new init.
And this init is going
to take a directory,
which is just gonna be a URL.
And we're gonna look in this directory,
and load up our documentNames,
whatever files, with whatever
files we find in here.
Now, what's gonna be our name?
You remember our Store has a name here,
where init with name so
we're providing the name,
this is the name of the Store itself,
not the names of any Documents,
just the name of the Store itself.
We could have that be
an argument here as well
or another argument name but for fun,
I'm gonna set our name here
to be equal to our
directory's lastPathComponent.
Now, this might not really be good
because maybe this is
an internal directory
that stores our documents.
So I'm gonna make this eventually
be our Documents directory.
So this is gonna look pretty good
because I think that's called Documents,
but I mostly wanted to do this to show you
what it looks like to grab
the last component out
and put it in something that
we're gonna see in the UI.
But, in reality, probably want another
argument there for the name.
And so we're gonna grab
onto this directory.
So let's say self.directory
equals this directory,
put that into a little
private var directory,
which is a URL.
So this URL, and this directory
points us to where all of
our Documents are stored.
We're obviously gonna wanna
create this DocumentStore
likely with our Documents
directory in our sandbox.
But we'll get that a little bit.
Right now, we're gonna
make this DocumentStore
so it can work with
any directory anywhere.
You pass what directory
you want to its init,
and it's just going to open it up,
and look at all the
files that are in there.
Now, how does it open it up,
and look at all the
files that are in there?
How do we find out what
files are in the directory?
Well, we're gonna use
that FileManager thing,
and it's got a really
cool little function.
I'm gonna call this documents,
let documents equal FileManager.default,
that's the shared one.
Give me the contentsOfDirectory.
You can see there's a
couple of them right here.
I want this one, directory atPath,
my directory's path, directory.path.
Path right here is a URL
method or var, actually,
and this var returns this URL as a String.
And that's a lot of the FileManager things
take these paths as Strings,
some of them take URLs,
but some can take Strings.
This one happens to be convenient
because it just takes this
nice one argument right here.
Now, you can see we have an error,
call can throw, but it's
not marked with try.
So I could say try? here,
but I'm gonna actually
do the do catch here
because I want you to get a look at it
every once in a while, so
that you know what's going on.
Otherwise, I'll send
you out of this class,
and you'll just always be doing try?,
and you won't think about the fact
that we can do try
without a question mark,
and catch the error.
I do this one other demo,
and we'll do it again here.
Now, I could just for now,
I would just maybe print this error out,
"EmojiArtDocumentStore couldn't
create store from directory"
Let's print out the directory.
That was attempted here,
and we'll even print out the
error's localizedDescription.
And we might want to, for example,
make this be a failable
initializer perhaps,
and then, return nil in here.
So it's kind of how you do error handling.
You really want to think in general,
what your strategy is,
are you gonna notify the user,
are you gonna try and recover?
I'm kind of a fan of trying to recover
as much as possible but by the same token,
you don't wanna mislead your user
into thinking that somehow,
something that they thought was working,
did not work or vice versa.
All right, so we've got our Documents.
This is an array of Strings.
If you look at it here, array of Strings.
These Strings are just the names
of all the files in this directory.
Couldn't be simpler.
So let's go through each of those.
And for each one, I'm going to create
an EmojiArtDocument from
what I find in that file,
EmojiArtDocument, and it's great.
We just added EmojiArtDocument URL.
So how do I create a URL to this document?
This document is a String,
it's just the name of the file,
and I have the directory right here,
so I'm gonna take the directory,
and I'm going to append a path component,
which is that document name.
So I'm just creating another URL here
from our directory URL by adding
the document name back in.
And I'm doing this for
every one of the documents
that I found in that
directory, super simple.
When I have this document,
I'm gonna add this to
my own documentNames,
this EmojiArtDocument,
and its name is document.
Now, we've updated our
internal data structure
to reflect what's in the file system,
and we've done this at initialize time.
Now, we still have quite a
bit more work to do up here
with all these adding, and removing,
and changing names of documents.
we're gonna have to keep the
file system in sync up there
but at least we kinda got
started here on the right foot
by having our internal data structure
load up from the file system.
That's pretty much all
is necessary to do that.
So now, let's go back to our SceneDelegate
over here, where we are
creating this DocumentStore
called EmojiArt that we
get out of UserDefaults,
and instead of doing that, let's go
and let our store equal
an EmojiArtDocumentStore,
whose directory is a URL,
and this URL is going to
be our Documents directory
in our sandbox, that special directory.
Everybody remember from
the slides how we do that?
Say let url =, again,
we're gonna use the
FileManager, the shared one.
In the slides, I showed you this one
URL for SearchPathDirectory
in appropriateFor create, remember that?
When you said different one
here, it's called URLs, plural.
And you still specify the
special directory you want.
So I want the documentDirectory,
careful there.
You don't want to do
documentationDirectory,
you want documentDirectory.
What's a documentDirectory?
And we always again in userDomainMask,
iOS is an operating system
for individual devices,
so we're always giving the
user's Document directory,
not the shared one on the network,
or something like that.
Now, this URL's version
returns an Array of them,
again, because on other platforms,
you might have multiple masks here,
user mask, network mask, so you might
be getting multiple responses.
Here, we're only going to get one.
And I'm gonna throw an
exclamation point on here
in the assumption that I always
have my documentDirectory here.
And putting an exclamation point here,
that's not true, and this crashes.
I'm gonna at least find that
during my development period,
but when it comes time to ship,
maybe, I'll do something
where I use this kind of store
if for some reason I can't,
I don't have a documentDirectory.
So, this is it.
We've got the Store, let's run!
See what's going on here.
All right, so this is probably working
because there's no documents here.
See, we have no documents
in our file system,
our documentDirectory in our
sandbox is probably empty.
So all this well.
Yeah, maybe we could
add some documents here,
but this is not actually
adding them to the file system.
If I stop and rerun, these documents,
no change, of course, because
"+" is not doing anything,
it's just adding it to our
little local documentName
data structure here in our DocumentStore.
And so this is having no
effect on the file system.
So we need to update these things up here,
addDocument, removeDocument, even setName,
to affect the file system as well.
And that's gonna be great
because we're gonna get a chance
to see how do we affect the file system.
We already learned how to read
the contents of a directory.
Now, let's learn how we, for example,
remove files and things like that.
So let's do addDocument first,
so we can get some documents going
in our documentDirectory here.
And addDocument has a little bit
of an interesting aspect to it
when we start storing in the file system.
When we store it in UserDefaults,
we can have two, or
three, or four documents
with exactly the same name.
And that's perfectly fine.
But in the file system,
that's not allowed.
The file system, you only get one document
of a certain file name.
That's just the way it is.
That's the way file systems work.
We distinguish the
things in the file system
by the name of the file.
So I'm going to have to be careful here
when if I'm dealing with the file system,
to not just use the name
that's passed to me,
or even just "Untitled" default.
I have to make sure that
name doesn't already exist.
What I'm gonna do here is if it does,
I'm going to create a unique name.
So I'm gonna take here,
let uniqueName equal this name
that you're asking me to do, unique-ified,
and I have this nice function that I wrote
called withRespectTo documentNames.values.
So this unique-ifies,
this String with respect
to these other Strings.
And these other Strings
are the values of my documentNames.
In other words, the documentNames
I already know about.
Let's take a look at this
uniqued withRespectTo real quick.
It's down here in EmojiArtExtensions.
It's a real simple little function.
It just creates a copy of myself,
this is in String right?
Copy of myself, and while
I am in this otherStrings,
I'm going to increment myself.
Now, what is incrementing myself?
That's a new thing I added to String,
where it puts a number
at the end of the String
and just increments it as necessary
to finally find a unique number.
So if my name is "Untitled",
and there's something else
already called "Untitled",
I'm gonna eventually become "Untitled1"
And if my name is "Untitled1",
and I do incremented, this little code,
which you can look at later,
it's kinda fun code actually,
is gonna go "Untitled2",
"Untitled3", "Untitled4",
until we find a unique name.
And so it's just kind of
simple, simple tricky thing.
But I actually am showing you this,
and I used this because I want
to show you something else.
If we go back to our Store,
this line of code that I wrote
that did that, didn't actually work.
It says cannot convert value of type
Dictionary.Values
to expected type, Array of String.
Yeah, indeed my EmojiArt
extensions over here
takes an Array of otherStrings,
which it wants to be unique withRespectTo.
But back here, this is
not an Array of Strings.
If you have a Dictionary
like documentNames,
and you ask for its values,
you do not get an Array of these things.
You get a special type that
is a Collection of these things.
So we're using functional programming here
to create, return something that
it's not an Array, but it is a Collection.
Now, my unique-ified really
should be just as happy
to work with a Collection
of Strings, as an Array.
For example, a Set of
Strings should work here.
If I want my name unique-ified
with respect to a Set of other
Strings, that should work.
And certainly, I'd like to be able to pass
these values of a Dictionary,
whatever this collection is.
So how can I make this over here?
So this doesn't take an
Array of String anymore,
but it actually takes a Collection,
and where that Collection
has Strings in it.
To do this, this is not
object-oriented programming,
so it's not like a Set, an Array,
inherit from some class or collection,
this is functional programming.
So we're gonna do this way
we always do these things,
with a don't care and constraining.
So this is constrains and gains.
So how are we gonna do this?
I'm gonna create a don't care here,
which I'm gonna call StringCollection.
So I'm gonna unique-ify with respect
to some StringCollection.
Now, I'm not totally don't care on this.
It has to be a Collection,
because I wanna do contains on it,
and it also has to have Strings in it,
because I'm trying to unique-ify
withRespectTo Strings.
So how do a don't care, with
just a function like this?
This is not don't care on
String or something else,
it's just this one little
function on its own,
wants to have a don't care.
It does that with .
It's very similar to doing it on a type,
a struct, or whatever,
just do it right here.
And I can also do the where.
So I'm gonna do over here,
where this StringCollection
is a Collection, gotta be a Collection,
and also, where the StringCollection's
Element equals String.
So this.Element is a
don't care in Collection.
Remember that, a protocol like Collection,
it can have a don't care associated types.
And so, it has one, it's just the element,
and Array has the same one,
and a Set has this one
because they're getting it
from Collection 'cause they implement
the Collection protocol.
So I'm just constraining
this to make this work.
Look, this is how it works.
Because contains is in Collection,
and of course, we can do things to it here
that are String-oriented
because I'm making sure
this is a Collection of Strings.
And not only that, but back here,
when I recompile, this is gonna work.
This is no longer complaining
because this is a Collection of Strings
because this is a Dictionary
that has Strings as its values.
Yeah, I just wanted to
take a little detour
from what we were doing to just throw
another functional
programming thing at you.
All right, so we have a unique name here,
so we can add our document
with this uniqueName.
So let's do that.
I'm gonna create a document here
as a little local var.
And then, I'm going to say,
if I can let the URL,
this is gonna be the URL for this document
that I'm gonna add, I'm
gonna add a new document,
so I'm gonna create a
URL for that document.
It's my directory that my store is in,
appendingPathComponent,
which is that uniqueName.
So this is the URL that I would want
to create this new EmojiArtDocument is.
Now, I'm doing if let
because maybe my directory
is nil because someone did init named
instead of doing init directory.
In this case, my store isn't
based in the file system,
so my URL is nil, so I can't
get a URL for this document.
But if I can get a URL for this document,
then, the document is just
gonna be an EmojiArtDocument
with that URL, let's create
a new EmojiArtDocument.
If not, if we're just doing
the UserDefaults thing,
then, the document is just
a blank EmojiArtDocument.
Brand new and 'cause we're in addDocument,
we're adding new documents.
Let's pay attention to our
error here first though.
Oh, yes indeed, we don't wanna be adding
blank document here, we're going to add
this document that we just created.
That's why your warnings,
you gotta always pay
attention to those warnings.
And also, let's make
sure that we are using
the uniqueName that we just created
as the name of the document there.
All right, so we have no documents here.
Let's see, Plus, hopefully
that created a file
in our file system for that.
And how about another one?
Whoa, we got the unique, another?
Whoa, it incremented, okay, so our little
increment code worked as well.
Now, hopefully when we
quit and come back here,
those three files are gonna
be in the file system.
And they are excellent.
Okay, next step, deleting.
So we go over here and say
let's get rid of "Untitled1".
Oh, the bugger worked, nice.
Now, let's rerun.
Oh, "Untitled1" is back.
That makes sense, right,
because we didn't,
when we hit "Done" here and said "Delete",
we didn't actually delete
it in the file system,
we deleted it in our
document names, that's it.
All right, so we gotta go
fix removeDocument here
to delete the thing from the file system.
That's probably the easiest
of all the things we need to do.
We're just gonna see if we can
get the URL for that thing,
which we have to get from its name,
that's our documentNames
for that Document.
And then, letting the
URL equal our directory,
and appending the
PathComponent of that name.
So, if this Document that
we're trying to remove,
it's already, we've got a name for it,
and we can make a URL out of it,
meaning that we're a Store
that's in a directory.
We're just gonna remove it.
And that's another thing that
our FileManager can do for us.
FileManager.default shared one,
removeItem at URL, kaboom.
Now again, like most of these
FileManagers things, this can throw.
This one, in this case, I'm just going
to do the try?
because we're trying to
remove this document,
and I can't remove it.
I'm not sure what I'm gonna do about it.
I supposed I could look at these errors,
and see if there's one that's
recoverable in some way
but probably unlikely.
That did it, removeDocument done.
Now, let's try and remove "Untitled1".
All right, done, and rerun.
Gone, nice, add this
back, add another one.
Let's go here, delete "Untitled3",
let's delete "Untitled", done, rerun.
Whoa, okay, so the last one I
think we have here is rename.
So if I go edit here and I say,
"I wanna change this
back to being "Untitled",
"and I hit Done," looks like it works.
Three run, oh no, "Untitled1" is back.
So again, I'm renaming.
It's having no effect on the file system,
so it just comes back and shows you
what was in the file system when we rerun.
Let's change setName.
This one's just a matter
of changing the URL
for this document right here to be a URL
that works for this name.
So if I can let url equal our directory
by appending the
PathComponent of this name
that you wanna set as the new thing,
then, the document.url = url.
I've just changed the place,
where this is happening.
Now, I know, one other thing though,
what if this is a rename?
Okay, someone said the name of a document,
and it had, was already around somewhere
in our document name
with a different name.
So good to be careful of that.
How about if we remove the old document?
If we remove this old document
before we change its URL,
it'll remove it from its old URL,
and now, we put it in its new URL.
But there's one other thing with naming,
it's a little tricky,
similar to addDocument,
which is what if you try to set the name
of a Document to the same name
as some other Document already has.
Oh yeah, well, we could
try some unique vocation
or something like that.
But I actually think the strategy
I'm gonna use is I'm just
gonna reject that request.
I'm simply just going to ignore it.
Again, maybe I wanna enhance this func
to return a Bool, whether I was able
to actually do this rename,
and then, the UI could
put up an Alert, whatever.
We're doing a demo, so I'm
not gonna do any of that.
I'm just not going to allow
a rename to happen here,
if you're trying to do
one in the file system,
and that name exists.
So the way I'm gonna do that
is I'm gonna say, if the, I'll do this
if the documentNames.values
does not contain this name,
then, I'm happy to rename this for you.
Otherwise, I'm not gonna do it.
And in fact, otherwise,
I'm not gonna do anything.
So let's put this one down
here in an else like that.
And then, do this up here
only if this don't have
this name conflict.
Well, let's see what happens in the UI
if I just take this strategy,
just not allowing renames,
that to another name.
So here we go, let's try.
Well, first of all, let's
see if rename is working.
So here I'm gonna do "Untitled1",
I'm gonna rename it to be "Untitled".
That's not the name of something else,
that's good, let's rerun.
Oh, it renamed it in the file system.
Okay now, let's try, and
take this "Untitled2",
and call it "Untitled", done.
Oh, look, it didn't allow me to do that.
But when I went back, it was here.
That's interesting, so
let's try "Untitled1".
Yeah, that works, okay,
let's add a few more.
Let's try and rename
"Untitled4" to be "Untitled2".
Done, oh, wouldn't allow
it, just went back.
But when I go back to Edit,
it's still holding onto that "2" for me.
So maybe I really want
"21" or maybe I want "2a",
something like that.
And that, it allows.
So it's not too bad that we just rejected.
I probably would prefer
a little Alert there,
maybe just because the user
might be a little confused.
They're here, and they said this,
and they hit "Done", and
they think, "Oh, they did it,
"but oh, there's that "2a", it's back.
"What's going on?
"I tried to rename that."
Yeah, they may or may not notice it
there's another one with that name.
So maybe an Alert that name is already
in use would be good here.
But we're here to learn about file system,
and I think we did that.
We showed a lot of different
parts of file system here.
We started out over in our document,
just learning how we can use Data,
and URL to change our reading,
and writing to be going
out to the file system,
instead of UserDefaults.
That was super easy over here.
And then, when we went to the store,
we learned a lot of
things about FileManager,
the shared FileManager like how to get
all the contents of a directory,
and then, how to do things
with the FileManager
like remove files.
So that is it for this demo,
and hopefully, you'll have a chance
to integrate this into your final project.
It's pretty straightforward to do,
and usually you do have
storage requirements,
and I'd rather not see
all of your final projects
storing everything in UserDefaults.
I definitely would like to see
some Core Data, and or file system,
and maybe even CloudKit,
that would be great.
- [Announcer] For more, please
visit us at stanford.edu.
