(dramatic music)
- [Animaker Voice] Stanford university.
- [Lecturer] All right, here we are,
lecture 12, Stanford
CS193p Spring of 2020.
Today's topic is Core Data,
which is an object-oriented database.
And we've been doing a lot of
functional programming this quarter.
Pretty much all the stuff we've been doing
has been functional programming.
We're gonna switch over now
to doing a little bit of
object-oriented programming.
Probably won't even notice
the difference here.
Although Core Data is based
on object-oriented programming
and Swift supports both object-oriented
and functional programming equally.
And we're essentially
gonna use this Core Data
infrastructure to store
and retrieve objects
or classes in a database.
Now, there's a very mature
technology out there,
it's been around for a long
time to store large data sets,
it's called SQL.
But programming in SQL is very
different than the kind of
programming we're doing in Swift,
SQL is a language and
it has its own syntax
and it's quite quite different.
And it would be a bummer for
us if all of this learning
we've done about how to program in Swift,
couldn't be applied to storing data.
So we're gonna get the
best of both worlds here
because Core Data actually
does its storage in SQL,
but we are going to interact with our data
entirely in an object-oriented way.
We're not even gonna need to
know a single SQL statement
to make this happen.
Now, the heart of making this
work in Core Data is this map.
And it is a map between the
objects and the vars on those
objects, and the tables and
rows of a relational database.
Now, if you don't know what
relational databases are
and all that, you really
don't need to know
because you're gonna be focusing
on the objects and vars,
not on the tables and rows
in a relational database,
and Xcode has a built
in editor for this map,
which is really great.
And this editor doesn't just
let us specify the objects in
vars, it lets us graphically
create the relationships
between objects, because that's
really the important part,
not just to have objects
with vars on them,
but you have objects that have
relationships to other objects.
And we'll see that some examples of that,
in a minute and in big time,
we'll see that in the demo.
So then what, you create this map,
what can you do then?
Well, Xcode behind the scenes
is gonna generate the classes,
right, the code for the classes,
that represent those objects and vars
you created in the map.
And we get to use extensions
just like we do with any other
kind of data structure.
We get to use extensions
to add our own methods
and our own computed
vars to those classes,
because, the storage vars
are stored in the database.
Then these objects that
have been created for us
and that we've extended,
service the source of data
for the elements in our UI.
So all of our SwiftUI,
that's doing everything,
it's obviously putting data on screen,
right now we store them in
ViewModels where we have
Arrays and dictionaries
and all that stuff.
Well, now this data is
going to be coming from
this database and we're just
gonna be accessing these
objects to put it on screen.
Perfectly naturally, like
we would normally do.
So what are the features of Core Data?
Of course, it has features
for creating objects.
You have to be able to create
objects in the database,
and it very naturally
lets you change the values
of the vars on these objects,
so you just change them.
They're just vars and you
set them equal to something,
boom, you just change
data in the database.
And it even has beautiful
infrastructure for establishing
these relationships between objects.
It's just a matter of
setting a var basically
to create a relationship
between one object and other and
by relationship I mean things like
a flight has an origin airport.
So flight objects and airport objects,
they have a relationship.
And there's of course a
way to save these objects
and really important
that you can fetch the
objects from the database
based on certain criteria.
You wanna say, I want all the flights that
match this certain criteria,
arrived at this certain time
or whatever that might be.
It has a great simple API for doing
the fetching of the objects.
Now it has a whole lot of other
database features on top of
it, optimistic locking
and all this things.
We are not going to talk about this.
This is an intro course,
so we're just introducing the
concept of Core Data here,
but there is a lot of stuff in there
to take this to pretty high levels.
Now let's talk about the
integration of SwiftUI,
with this Core Data database.
The objects that we create in the database
are ObservableObjects.
They're essentially
little mini ViewModels,
and there is also a very
powerful property wrapper
in SwiftUI called @FetchRequest,
which fetches these objects for us.
So this FetchRequest is more
than just a one-time fetch.
It's kind of a standing query,
that's just constantly trying
to match whatever the criteria
you're talking about are,
and returning whatever's in the database.
So as things get added to the database,
if they match the criteria
of that FetchRequest,
then it's going to update your UI.
That way our UI is always in sync
with the database
really, really fantastic.
So how do you get Core Data
into app into your app?
How do you set it up?
Well, the real way to set this up
is when you create a new project,
there's that button that you saw
called Core Data, use Core Data.
And you're gonna click that,
and it's going to do a
little bit to set you up.
Now, if you've already got
an app and you've decided,
oh, I wanna add Core Data to it,
I recommend going back
and creating a new project
and clicking this button
and then moving all your
source code over because this
button does such a really
nice job of setting things up.
So what does this button do?
Well, it creates a blank map for you.
This map that I told you
has a built in editor,
you got a blank, one of those,
so you can start adding
your objects and your vars.
It adds a little bit of
code to your AppDelegate,
which I'll show you in the demo.
You haven't seen it yet.
It's really nothing, much
of interest in there to us,
but it does add this little bit of code
that creates the database
store, the actual SQL database,
that the stuff is gonna be stored in.
And so you don't ever need
to go look at that code,
even, it just all happens automatically.
I just wanted to let you know that
it does do something in there.
Now it does add a couple of lines of code
to your SceneDelegate,
right where you create
the content View there.
One of the lines of code goes
and reaches into that store,
the SQL store code that was
in AppDelegate and it grabs
something called an
NSManagedObjectContext.
And this context is
crucial to using Core Data.
It is the window through which we see
that all the objects in our database,
and you're gonna see that we
need to contact whenever we
create objects, we do
fetches, any of that stuff.
So this two lines have
SceneDelegate code order,
go grab that context out of the store.
And then the second one,
interestingly is to pass that
context into your SwiftUI
Views via the Environment.
And we already saw
Environment, for example,
with the edit mode.
And this is using Environment again,
so that all your SwiftUI Views
will have this context so
they can see the database and get objects
out of it and all that.
So that's added to your SceneDelegate.
And that's pretty much
all the set up you need,
because now you can create
objects, fetch them,
whatever you have the window
into the database you need,
you've created your map,
whatever the objects in vars you want,
and you're on your way.
So let's take a look at that map.
I'm gonna, since the demo is gonna create
detailed map with airports
and airlines and flights
in our on route, there's no
reason to go over it here,
but just to give you
an image in your mind,
what it looks like, you know,
here's an example on the left,
we have a, looks like that is a Flight
and it's got some attributes
like when it arrived
and or it's going to arrive,
and it's ident, and even
some relationships to other
objects like, the destination airport,
the origin airport airline.
And then on the right,
you can see this graphical map
of the relationships between
Airports, Flights, and Airlines.
For example, if you see Flight,
it's got this relationship to Airport,
which is the origin and
another relationship,
the destination, and
notice that the Airport has
relationships back along the same line,
which is the Flights from this Airport,
and the Flights to this Airport.
Now, those origin and destination
relationships in Flight,
those are just Airport objects.
That's the type of those vars,
all those little black words
you see there, code name,
short name, aircraft,
those are all just gonna be
vars inside my Flight object,
my Airline object, my Airport object,
Airport object, and destination,
origin are just vars
and their type is gonna be Airport.
Now the flightsFrom and the flightsTo,
they're a little interesting because
there's multiple Flights
to and from an Airport.
So those are actually
gonna be Sets of Flights.
That's the type of that var, it's a Set,
kind of an old Objective-C-style Set.
And we'll talk about that in the demo,
but that's what's going on there.
So all of these things that
we see on the screen are just
turning into objects, Airline,
Flight, and Airport objects,
with vars, aircraft,
arrival, departure file,
those are all just vars
on each of these things.
That's what this map is doing.
And that's really all there
is to it with the map.
So once you have this map and
now you have the window that
was passed into your Environment
that we talked about from
your SceneDelegate, what can you do?
Well, you can get that window,
that managedObjectContext
via your Environment,
it's \.managedObjectContext.
And now you have this context var,
and you can use it to do
things like create an object.
You just do that by saying Flight,
with the argument context so that knows
the window into the database
and it'll create you a Flight.
Now you have a Flight this
green flight var here,
and you can just start
setting the vars on it.
Like the aircraft, set it to "B737",
Boeing 737 aircraft.
And here's another thing
I'm creating an Airport
called KSJC, and I'm
just setting it's ICAO,
which is its unique identifier
for an airport to "KSJC".
And I can even do pretty
powerful var setting
like if I set the
Flight's .origin to ksjc,
'cause kscj is an Airport,
and the origin is a var
that's of type Airport, then
that sets that relationship.
And that will also affect
the opposite relationship of
ksjc's flightsFrom, will automatically get
flight added to it.
So you don't even have to
worry about the balancing,
the two sides, one's a Set of
Flights and one is the origin.
It automatically keeps both sides.
And if I had added it
to the Set of Flights,
then it would set it as the
origin on the other side.
So it's pretty cool system in that way.
So it's just objects and vars.
So you just that's looks all like
objects and vars to us here.
We don't know anything else
about SQL or tables and rows,
just objects and vars.
Saving it, really easy, context.save(),
this throws, however, now
why would trying to save it
throw? Well, your disc
before could be full,
for one thing, it's extremely unlikely,
but your iOS device doesn't
have unlimited storage,
but there could be possibly
other errors, but you know,
generally this is not going
to fail, but it can throw.
So that's why we have to put
the try on there when we save.
Now, what about fetching objects?
Getting them out of the database?
Well, we do that with this
very important objects
in Core Data called an NSFetchRequest.
All right.
And it has a don't care there,
which is the kind of thing
that you want to fetch.
And you create one just by
specifying the name of the thing
in your map, that is that
thing you're trying to fetch.
So Flight in this case is
what I called in my map,
the objects that are Flight.
So this is gonna create
a FetchRequest that goes
and fetches Flights.
That's what this request is.
Now how does it know which
Flight to go and get?
Well, this request has a
var in it called predicate,
and you assign it an NSPredicate.
You're definitely gonna wanna
look in the documentation
of NSPredicate and see all
the incredible wide range
of things it can do to specify
which Flights you want.
So for example, I've
created a predicate here,
predicate kinda has a
little bit of a printf
kind of feel to it.
This has been around a very long time,
well before Swift ever existed.
So it's a little printf-y.
And so my predicate here
is that I want arrival is
before something, and the
origin equals something.
And then I provide the somethings.
The first something is
the current date and time.
And the second one is ksjc.
So this predicate means,
show me all the things that
have arrived before now.
So they're already arrived,
and their origin was San Jose.
They came from San Jose.
So that's the predicate I'm doing here.
So it's a little bit, you know,
texty kind of oriented predicates there,
but you have to definitely
have to go look in the
documentation for
NSPredicate to understand
all the things that it can do.
So that's specifying which of the flights
I want in a database.
Now there's one other piece of
requests that you need here,
which is something called
the sortDescriptors.
And that's because when we
make this request into the
database, it's gonna
come back as an Array.
And that Array has to be sorted.
And we specify the
sortDescriptors so the sorting can
happen on the database side,
SQL database is super
good at sorting things.
And so we're gonna let it
do the sorting if possible.
And we do that with this
sortDescriptors var,
it's just an Array of
these NSSortDescriptors,
and a SortDescriptor
simply say, what var, ident
in this case, that's
the Flight identifier,
that you wanna sort by,
and whether you want it
ascending or descending, right,
alphabetical order A to Z or Z to A.
How do we then ask our context,
our database, go fetch
these things for us?
We do that with the fetch function.
So we say context.fetch this
request and it going to go
out to the database and go
find all the flights and match
this arrival and origin
predicate that we have,
and return them to us.
Now, this fetch also can throw,
just like save can throw,
so can fetch, and so we'd need to try it.
And a lot of times we'll do
a line of code like this,
let flights = try?
and then the context.fetch.
If we do this,
then that flights var
is either gonna be nil,
if the fetch failed, empty
Array, if it did not fail,
but no flights matched our predicate.
Otherwise it's just an
Array of flight objects,
the ones that matched.
So again, very object-oriented.
I'm just getting these Flight objects
that match my predicate.
I just threw all of this on one slide,
just so you get a feel
for all of the code.
And so you have kind of a
quick reference guide to it,
but we are of course,
going to do a big demo,
shows all this stuff happening.
Now what about the integration
with all this stuff
and SwiftUI?
Well, there's two major
points of integration
we talked about.
One is this ObservedObject.
When you have a flight, as
a var in one of your SwiftUI
Views, it's going to
be an @ObservedObject.
You can then use it to do
like Text(flight.ident) here,
and just show the ident and
apply whatever you wanna do,
it's just an object.
The ObservedObjects, by the
way, as far as I can tell,
do not seem to fire automatically
when things change in the database.
So if you want them to change,
you need to explicitly
call objectWillChange.
But these do make these Flights
and Airports and Airlines
feel like little mini ViewModels.
And that's really what they are.
They're little ViewModels,
on a little flight,
but a lot of times what we
wanna show in our UI is more
than just one Flight or one Airline.
We wanna show all the
Flights, for example,
that match a certain
FetchRequest or Predicate.
And so there's another
really important feature
in SwiftUI, called the
FetchRequest property wrapper.
Now this property wrapper, you
create it with the arguments,
either the same arguments
that go to a FetchRequest,
which is the entity,
remember that entity
name thing was Flight,
and the sortDescriptors and the Predicate.
So you can create a FetchRequest that way,
or you can actually create a FetchRequest
just by giving it a
FetchRequest you created,
either way you're specifying
what things you want to fetch.
And this is a property wrapper, right?
So it's wrapping some var.
And the type of the var
that wraps, is called a
FetchedResults, also has a don't care,
and that don't care of
course is what kind of thing
it's supposed to fetch.
So that better match
whatever your FetchRequest
says that it's trying to fetch.
Now this FetchedResults
var, it's a Collection.
So you can pass it to ForEach
as you'll see in a second.
But so it's not an Array,
but if you wanted to turn
this thing into an Array
really easy, you could pass
it to an Array as Array's
initializer 'cause an
Array knows how to take any
Collection and turn it into an Array.
You could also send it
messages like sorted,
sorted can be sent to any Collection,
and it returns an Array.
And that Collection is full of
those kinds of objects which
you're fetching, Flight
objects or Airport objects.
Now what's cool about this
var that you're creating here,
Flights or Airports is that
it's continuously trying to
fetch that FetchRequest.
All right,
this is not a one-time fetch,
where it's gonna fetch out
and give you the results.
It's continuously doing this fetch.
It's like a standing query.
And so every time a new Airport
or a new Flight is added
there, or even if an existing
Airport or Flight changes
one of its vars, so that now
it matches your predicate,
it's going to update.
And when it updates,
your SwiftUI is gonna get
invalidated and redrawn.
And so your UI is always
going to be showing
what's in the database.
You don't ever have to
tell it to go fetch.
And this is all part of the
declarative nature of SwiftUI.
You don't say to do things,
in SwiftUI you just declare
the things are a certain
way, and they always are that way.
And of course, with Core Data,
we always want them to look like whatever
is in the database.
So we tend to use @FetchRequests,
sometimes even when we're just
fetching one object, right?
We might be having the
predicate, just be like ident,
equal something for a
Flight, that's ever only
gonna match one Flight.
But the great thing is
if that Flight changes,
this is going to be updated.
That's why the fact that the
ObservedObjects up there,
we were talking about don't
seem to kind of do their
objectWillChange when something changes.
That really is not a big problem,
because we would just maybe
have a FetchRequest that
fetches that one thing.
And that will change, because
FetchRequest is always
tracking what's happening in the database.
It's really a fantastic property wrapper.
This API that you're seeing
right here is a really elegant,
simple one thing to make it
so that your UI is always
matching up against Core Data.
So that's really, really cool.
By the way I show the
FetchRequest up there.
I'm creating it.
When I declare the vars with
the FetchRequest I want,
but of course you could just
declare it and I'll show you
this in the demo and then use your init
and set the _flights, right, _flights
is the actual struct,
the FetchRequest struct
to be something you create then.
So if you're passing the
predicate in, for example,
that you wanna use, you
can pass it into your init,
and then your init can just
initialize that FetchRequest
thing, just like we've
initialized, Bindings this way,
State with its wrapped value,
you can do the same thing here.
And we'll see that in the demo.
And speaking of which, lets
dive right into the demo and
show you what it looks
like to do all this stuff.
Our goal in this demo is
to convert our Enroute,
to use our Core Data database
and store all the information
that comes down from
FlightAware into a database,
and then build our whole UI by
just looking in the database.
Right now, we actually fetch this stuff.
And as it comes back in FlightAware,
we're showing it in our UI directly,
here we're gonna fetch
it all from FlightAware,
put in a database, and then have our UI,
totally just looking at the database.
And this is a common kind of
paradigm for developing an
application that fetches data,
instead of having to try and
to keep track with it
and get it all the latest
information all the time,
you just throw it in the database.
And so you're always
looking at the database
and makes your code a lot
simpler on the UI side.
That's exactly what we're gonna do.
Now, if you wanna use
Core Data in your app,
the best way to do it, is
when you say "New Project"
over here, click this
button, use Core Data.
If you click this button, use Core Data,
you're gonna get the small
amount of very important code
that hooks you up to a database,
this additional button,
by the way you use CloudKit,
that's pretty cool.
That will make it so that
everything you put in your
database gets mirrored up to iCloud.
And that way the user will
see it in all their devices
in Core Data.
Pretty amazing.
We're not gonna do that part of it.
This demo is already long enough,
so we'll just focus on the Core Data part,
but it's really not that complicated.
If you're interested in that,
you can certainly look up how to do that.
Now, if you've already started your app,
like we have with Enroute
and you wanna add,
Core Data functionality to it,
I actually recommend doing what I did,
which went right back and
created a new project anyway,
and then just copied
and pasted all my code,
dragged my files back
into the new project,
because this little switch,
it does a really good job,
by just setting things up.
And so let's look at that setup.
Let's see what it is
this little switch did.
And really added two
important pieces of code.
One is right here in this
thing called AppDelegate,
which we haven't even
looked at AppDelegate yet,
but we're gonna look at it right now,
and here you can see it's added
this thing called Core Data
stack with this persistent
container right here.
That's the thing that holds
your Core Data database.
And so we are going to use this,
to access the database.
In our SceneDelegate
where we create our little
FlightsEnrouteView, notice
it's added a couple of lines of
code here, this context,
which looks in that persistent
container, we were just talking about it,
get something called a context.
In this context is then
passed via the Environment
to all of our Views.
Now we want to be very sure
that when we put up a new
Environment of Views, like when
we use a sheet or a popover
or something like that, that
we pass this along to it.
Now remember an Environment
when you give it to a View,
all the Views that are in it's
body, actually in it's body,
get the same Environment.
So you don't have to pass them there.
But if you do a sheet or popover now,
you're kind of going
off to a new base body.
And so you do need to pass,
and you're gonna see that,
'cause of course, we do
do a sheet in this app,
we have that filter flights.
So this context is our
window onto the database.
Where is the database herself
or what is the database?
And the database is something
we designed using this little
folder right down here,
which is also provided
by that use Core Data.
Here it is.
This is all of the objects
that we're going to
create in our database.
Now, remember this is an
object-oriented programming layer
on top of, in this case, a SQL database.
A normal relational database.
And so it's doing all the mapping for us.
We don't have no idea that
that's a SQL database and we
don't wanna know, we're just
gonna create our objects.
And that's what this tool right here,
this tool that's editing
this .xcdatamodeld file
is letting us do, define
what objects we want in the
database, what vars are on those objects.
So let's dive right in
and create our objects.
This is Enroute.
What objects do we have?
Well, we have the flights.
That's the main object,
all these flights that we're showing,
but we also have some kind
of helper objects like the
airline and airport objects.
So let's create Flight, Airline
and Airport objects here.
This entities is the list
of objects that we want.
And we add them down here
where it says, add entity.
So boom, there it is.
Let's call our first entity, Airport.
Let's add another entity,
call this one, Airline,
and our third entity,
which is a Flight.
So we've created the object right here,
but they have no vars.
This on the right, over
here is showing the vars,
especially this part of
the top of your attributes,
and none of our objects
have any vars yet of course.
Let's start with the
simplest one, Airline.
It only has three vars.
It has a code, and this
code is of type String.
So this is how we add a var,
we just press the plus,
give it the vars a name,
and then it's type.
It also has a name.
The name is also a String, and
that's the full name of the
airline like "United
Airlines Incorporated",
something like that.
But it also has a short name
in the FlightAware information
that comes back and that's
like, "United" just short, short
and sweet, not quite a nickname,
but shortened name of it.
And that's it.
That's all that comes
back from FlightAware
for Airline that we're
interested right here.
What about Airport?
So Airport had a very important
thing called the ICAO,
that is a String, that's
like KSFO or KDFW, KSJC
for San Jose, that's it's unique code.
You notice they all start
with K as airports in the
United States start with K.
So that is very important
one for us to have obviously,
but they will also get
kind of cool things,
like we get the latitude and longitude.
Those are Doubles.
Those come across from
FlightAware as numbers,
and so we're gonna store them here
in our database as Doubles.
Got some other stuff like
the location of the Airport.
So for San Jose, that's
San Jose, California
is the location of it.
And we also have the
time zone that it's in.
When we fly around up there
in the skies, all the pilots,
everything is going on in GMT,
right, Greenwich meantime.
So it's nice to know, the time
zone of the actual airport
we're going to so we can
convert to local time.
And then all the airports have a name.
Like I think San Francisco
International Airport
is its full name of SFO.
That's Airports.
What about the Flight?
Flights have something called
in FlightAware an ident.
This is like UA475 United
airlines, flight 475.
That its unique identifier.
We definitely wanna be able to uniquely
identify our flights.
Our Flights also have aircraft.
So this is like, a String
"737" or something like that.
Of course we have the arrival
time and departure times.
Those are Dates, go Date
here, arrival and departure.
And it turns out that Flights
also have another Date,
which is their filed departure,
because some flights are
still on the ground, they haven't left yet
either delay or they're
not scheduled to leave yet,
but they've filed, the pilot
has filed for a certain time
that they want you to depart.
So that's it.
These are our objects and their vars,
but there's one more piece
to our object-oriented puzzle
here, which is the
relationships between objects.
For example, a Flight has an
origin and destination Airport.
It has a relationship there,
that a Flight also has an Airline.
So we can add these relationships
in this section here,
relationship, but we usually
don't do it that way.
Instead we go over here, down
to this editor style in the
corner, and we switch from
this kind of table-oriented
one we're at, to this graphical version.
And this is giving us the same
information as we just had,
depending on a little graphical
version and these individual
vars, if we click on them,
we can see the types and
all that by going over here
and bring up this third pane inspector,
which we really have not looked at,
all quarter long because
SwiftUI doesn't really use it
that much, but really valuable here.
It's the fourth tab
over in this inspector,
and showing this location is
the var and here's this type
notice it says, "Optional" right here.
This does not mean optional
like Swift Optionals.
This means it's optional in the database.
So this unfortunate, this
has the exact thing name,
but it really has nothing to do with that.
All of these vars and
Swift, will be Optionals.
And that's really a design
decision there that's made
because your database might be
corrupted and you might think
that all of these things
have values, but they don't.
They're the object was started
to be created and it failed,
and so now these vars aren't set.
So that's why these are optional.
And I'm gonna show you a
little bit of a design strategy
or whatever in my code, to
kind of identify the ones that
really should never be optional,
and cover them a little bit
with some syntactic sugar, or
really I'm just gonna use a
computed var to make them not be optional.
You'll see how we do that.
But by default, all of these
are optional and it has nothing
to do with this switch right here.
So we can set other things,
default value and things like that.
But mostly what we're doing
here in this graphical
arrangement is to create
connections between these objects.
For example, let's talk about our Flight.
It wants its destination
airport, and its origin airport.
So how do we do that?
Well, we do that with the control key.
So I'm holding down control,
you can see the plus appears.
I'm gonna drag from my Flight,
over to my Airport here and let go.
And it creates a little
relationship here, a little line.
Now this is just gonna
be a var on each side,
on the Flight side,
it's currently calling
this var newRelationship.
I'm just double clicking
on it right there,
but I actually want this to
be my destination Airport.
I'm calling this var destination.
Now, what type is this
var going to be in my code
when Xcode creates all the
code that makes this object
stuff work, this is just
gonna be a var that is a type
Airport, because the Airport is an object,
we're doing object-oriented
programming here.
Each of these is a kind of
new object that Xcode is gonna
create the code for us.
It's really kind of cool.
Now what about on this side Airport?
Well, this relationship
here is really like
the Flights to this Airport.
And this is Flights, plural,
because if I have 20 Flights
all going to the same Airport,
well then this is kind of gotta be
multiple Flights or something.
How does that work?
Because this can't be a Flight.
This has gotta be like a bunch of Flights.
And indeed the way we do
that is we can inspect this
particular relationship and
say that instead of a "To One"
relationship, where there's
one Flight per Airport here,
it is a "To Many" relationship
and you can see that the
arrow became double arrow.
Now this is no longer just one Flight.
It is multiple Flights, which makes sense.
An Airport has multiple
Flights that are heading to it.
And what is gonna be the type of this var,
this var flightsTo?
Its type is gonna be a Set of Flights.
Unfortunately, it's not
a Swift Set of Flights.
It's the old Objective-C Set of Flights.
So you're gonna see us having
to do another little bit of
cleaning up, syntactic sugar,
computed var to make this into
be what we want, which we
really want it to be a Swift Set
of Flights, not an
Objective-C set of Flights.
And this is one of the few
places where Objective-C leaks
through in Core Data
unfortunately, but easily fixed.
Now I'm gonna do another control
drag from Flight up here,
for the origin Airport.
So that new relationship
down here is origin, origin
and up here, this is the
relationship on this side
is flightsFrom, and this flightsFrom,
is also a "To Many" relationship.
So both of these are
"To Many" relationships.
And Airline, same thing as
Flight, of course, every
Flight is operated by an
Firline, so we'll have
the relationship here,
called airline on this side.
And similar here, this is flights.
These are all of the Flights
being operated by this Airline.
So that is a "To Many" relationship.
Now all of this work we're
doing here to connect these up
and create these little relationship vars,
we can see it back in the
normal editor style right here.
So here's Airline.
It has flights, Airport
has flightsFrom and to,
Flight has destination,
origin, and airline.
So that's it.
That is how we build
our object model here.
All the code that's required
to make these objects exist and
have all these vars exist, is
totally done for us by Xcode.
And we just hit build here.
It goes through and creates all the code
that's necessary to make that work.
The only code we're going
to add is in extensions.
So we're gonna add some
code in extensions here.
This is object-oriented programming,
of course we want our objects
to do more than just have
these vars, we want it to
have some behavior as well.
We'll be adding that
behavior using extensions.
Let's go back now.
Now that we have our
whole object model here,
we have all the objects we wanna create,
lets go use them in our UI.
So here's our UI code that
we've built from last time,
it's still based on this
FlightFetcher mechanism,
and we're gonna completely
replace the FlightFetcher.
I'm gonna eventually comment
FlightFetcher out completely.
And instead we're gonna use
an object-oriented approach
to building our UI.
By the way, before we get started,
take a look at the top of this file,
you see import Core Data.
You wanna make sure you're
importing Core Data in any
files that you're gonna be
doing this Core Data stuff in,
otherwise symbols like
NSManagedObjectContext are
not going to be defined.
Now I'm gonna start with
this very important struct
right here, FlightSearch,
which currently is looking
up airports by their codes
and airlines by their codes.
And I'm gonna switch this
to be object-oriented.
My destination is not gonna
be a String like "KPAO",
it's going to be an Airport.
And same thing here.
My origin is going to
be an Optional Airport,
and my airline is going to be an Airline.
So this is going to totally
transform all of our code
to be object-oriented.
By the way, sometimes when
you build your object model,
the rest of your code, won't
get the message about it.
Like we still have
undeclared Airport again,
as Xcode can be building that
for us behind the scenes.
I usually will just try and build here,
sometimes that'll fix it.
Yeah that didn't quite get it.
Another one to do is go
back to your Model over here
and just do a build,
sometimes that'll get it going
see if that fixed it over here.
And it looked like it did.
So lemme see what kind of errors we got.
So we still have bunch of errors obviously
we have our whole code is based
on this thing being Strings.
And these are no longer Strings.
So let's just work our way
through the problems that we
introduced by changing
this to be object-oriented,
and fix them one by one.
And along the way, we're
gonna learn a lot about how to
create these objects in the database,
how to go look them up and
find them in the database.
That's all gonna be part and
parcel of using Core Data
to make this stuff object-oriented.
So let's start with the very first one.
It says here, cannot convert
value type String to expected
type argument Airport.
Okay, we already see one case
here where it's using a String
for an airport instead
of the Airport object.
So let's click on this, takes
us to our SceneDelegate,
and sure enough, right here,
we're trying to create a
FlightSearch with a String,
which again, destination applied
search used to be a String,
we just changed it to Airport.
So let's create a little local variable
to be our Airport here.
And I'm gonna say, let
airport equal something.
And this airport has to be
some sort of Airport object.
Now it is possible to create
an object directly using this
thing called the context, right?
The context we talked about is
this window into the database
that we get from that little use Core Data
button right here.
And this context is super important.
Everything we do is going
through that window of it,
to the database.
That's how we do
everything in the database,
but we're not gonna do that
here because it might well be
that this Airport that
I'm trying to create here,
which I'm essentially
trying to create the Airport
KSFO right, this Airport might
already be in the database.
So I kind of need a little function here,
which I'm gonna call withICAO,
which takes this String
and looks it up in the database,
and if it finds an Airport
object, it gives it to me.
And if it doesn't, it makes one,
and not only makes one,
but goes and fetches it
from FlightAware too.
So that's what this little
function is gonna do.
Now, how do we add a function
to our Airport object
that Xcode created for us?
We're gonna do it with an extension.
And we're just gonna extend this class,
this object-oriented class
Airport, to have this static
function, which lets us
look an airport up by name.
So let's go do that.
I actually created that file here,
Airport and blank file for now.
And I'm gonna say extension of Airport,
and here I'm gonna do this
static ofunc withICAO.
And it's gonna take that
little special code that I was
telling you about, and it's
gonna return an Airport.
And this is going to just look
up that ICAO in Core Data.
And if it finds it, return it,
if not, then we're going to create one
and fetch from FlightAware.
So that's what this
function is going to do.
So let's start with this, look up,
still complaining about that,
we won't worry about that for now.
We want to look up this code in Core Data.
So how do we look things up in Core Data
to see if there's something there?
We did this with a very
important object called
a FetchRequest, NSFetchRequest.
In fact, so I'm gonna let
request equal an NSFetchRequest
now, NSFetchRequest is
generic, it has a don't care,
which is the kind of object
you're trying to fetch.
Then you specify the
entity name as a String.
And this String is just whatever you put
here as the name of your entity.
So "Airline", "Airport" or "Flight".
So this is how you create a FetchRequest.
Now a FetchRequest is
essentially a guide that
we're going to use to have the database,
know which Airport we want
or which Airports, might be
looking for multiple Airports.
We might be looking for
Airports by time zone,
so we can get multiple Airports here,
and this request is gonna let us specify
which ones we want.
So the request has two
really important parts to it.
One is called its predicate,
and its predicate is the,
which ones do you want?
And you create that with an
object called an NSPredicate.
And it kinda has a printf
sort of feel to it,
where you give it a format
for what you wanna search
and then any arguments that fit in there.
So it's again pre-Swift.
So it doesn't feel super
Swift-y because it's printf-y,
but you'll get used to it really easily.
And for example, in our case,
we want the ICAO to equal something.
And what is that something?
The ICAO that is passed
to us up here, right?
This argument ICAO, I
want my predicate to be,
go look up this var, in the
database, with this value.
Then you can do things like,
and another one of them or
or something else equals something else.
And there's even more powerful
things where you can do
pattern matching and begins
with, and all kinds of things.
This predicate object, obviously,
if you're gonna do this
Core Data you're gonna
need to go look that up
in the documentation and
understand all the powerful
things you can do to go
figure out which objects
you want from the database.
The other thing that we want to know here
is the sortDescriptors.
Now, the sortDescriptors
are an Array of these things
called NSSortDescriptor,
and a SortDescriptor has two things.
One is the name of the
var you wanna sort by,
then actually, if I'm
looking for Airports,
I'm gonna sort them by location,
by the city that they're in.
And then the second thing
is whether you want them
ascending in order or descending.
And I do want this ascending.
Now, why is this important to have this
sortDescriptors here?
That's because when we look
up objects in the database,
they're gonna come back
as an Array, not as a Set,
but as an Array.
And it's an Array because they're ordered,
they're put in this order.
And why do we do that?
Why don't we just get a
Set instead of an Array?
Well, because sorting like
this can happen on the database
side very, very efficiently
and notice it's an Array of
SortDescriptor, so if you had
people in here and you had
first name and last name, you
could have a SortDescriptor
to sort by last name, and
then another SortDescriptor
to sub sort by first name.
That's why this is an
Array of SortDescriptors.
So that's all we need to do
to create this FetchRequest.
This is describing what we
want out of the database.
So how do we get it out of the database?
We use this little function,
let airports = context.fetch(request).
Now this context is one of these
things from back over here,
this context that we're
passing in our Environment,
this context here, that is
the window on the database.
If we wanna fetch something
out of the database,
we need that context.
So we're gonna have to pass
that along in with ICAO.
I'm gonna say here, context, context,
and then in withICAO, I'm
gonna have to have another
argument here, context.
And this context is of type
NSManagedObjectContext.
This fetch method though has
one other interesting thing is
that you have to try it
because it could fail,
no connection to the database
or something like that,
so it's possible it can fail.
Now, if you remember, if
we do try, we could do,
do and catch and try it in there,
and catch the error and see
what the error is and do things.
Here, if I try to do
this fetch and it fails,
I'm just gonna let this
whole thing return nil,
that's what try? hopefully
you remember that
from your reading about error handling.
That's what it does.
So this would be nil.
Now, if this doesn't fail,
but there is no Airport matching this,
then this is not gonna return nil or fail.
It's gonna return empty Array.
So calling fetch and
getting back an empty Array,
means I couldn't find it.
That's different from nil,
which means I had an error
trying to fetch this.
Now, if it does find it,
then this is going to return
an Array with all the Airport
objects in it, that match.
And this is a FetchRequest for Airports,
so this is gonna return
an Array of Airports.
Hopefully this never return
more than one Airport
because ICAO it's like, KSFO,
there should only be one
in the entire database.
That's why we're doing this
thing of looking it up first,
before we go create it.
So what I'm gonna do here is
kind of ignore the errors.
If this comes back nil,
then I'm gonna optional
default it to the empty Array.
If I were shipping this as a real app,
probably would catch this
error here and trying to figure
out what am I going to do because
I fetched this Airport and
it failed, probably if
this fetch fails though,
all your Core Data stuff's
gonna start failing so
you know, that would
be a top level failure
that you'd have to handle anyway.
So now this is going to be an Array.
It's either gonna be an empty Array,
if there was a failure or
if we couldn't find this,
or it's gonna be an Array
of hopefully just one thing,
which is the Airport object
that we've put in there
in the past for this thing.
So let's check those cases.
First I'm gonna say, if I can let airport
equal airports.first, first
element of the airport,
then I found one.
And so I'm just gonna
return it easy enough.
This is the first part of this lookup
It did this.
And so now if it found, return it,
otherwise we're in this case down here,
where we couldn't find it,
and now we need to create
one of these things
and then fetch it from FlightAware.
How do we do this?
I'm going to create the airport
first with just this ICAO,
I already have that information,
I don't have to fetch
that from FlightAware it
was passed into me here.
So let's create the airport.
You can say, let airport = Airport.
And of course we have to pass
that context because we have
to really tell the system
which database to create this
Airport in, but this creates an Airport,
and then I'm gonna set
the Airport's icao var.
This, our airport here has an icao var,
how do I set this var?
Amazingly I just say, .icao = icao.
And again, Xcode has built
all the code behind this
that makes this an
object that has this var,
that doesn't seem to
still be recognizing that
we can get it to do that.
So now we've got that to compile here.
So now we've created an Airport
and it doesn't have any of
these other vars that we
want in here, like latitude,
longitude, and all that,
that information all
comes from FlightAware.
So there's no way for me to say,
airport.longitude = something right here.
So it's just going to be nil.
Those values are gonna
be nil and the database.
Again, that's part of why
these vars are all Optionals,
even the ICAO is an Optional,
even though we're always
gonna be setting it here,
it's an Optional because might
not be set in the database.
So how do we fetch stuff from FlightAware?
In this FlightAware code
that I have over here,
one of the things we have
is an AirportInfo right here
in the AirportInfo,
there's this struct called
an AirportInfoRequest.
And it has function fetch,
you give it the ICAO you want,
and it will call you back later
once the information comes
back with, from FlightAware,
and give you an AirportInfo object.
So an AirportInfo object looks like this,
got the ICAO, longitude,
latitude all these things that
we want to put in the database.
I'm going to call a function here.
self.update(from:
airportInfo, context: context)
and this little static function
that we're going to do,
static func update(from: airportInfo).
I'll just call it info here.
This is an AirportInfo
object from FlightAware.
And then we need the context, of course,
because we're going to be doing
something in the database.
Anytime we're doing
something in the database,
we need the context to
know which database.
So in this update right here,
by the way, once we do this,
I'm gonna return this Airport right away.
Now, when I first return this Airport,
it's only gonna have the ICAO in it.
It's not gonna have all this.
This is asynchronous,
it's happening later.
So when someone says, give
me the Airport with an ICAO
and I've never seen it before,
I'm gonna give them
back a mostly empty one
that just has ICAO.
So I shouldn't,
I'm gonna have to make my
Airport be pretty tolerant.
All my codes are going to be
tolerant of all those other
fields being nil, which is
fine they're just gonna have to
check to see if they're nil.
But this one we can pretty
much always count on not being
nil, and we're gonna talk
about how we're gonna deal
with that in a second.
So update, how do we do
this update right here?
So this is happening something
time later, it comes back.
Let me see if I can let the
ICAO equal this info's ICAO.
So this is the info coming
back from FlightAware,
I'm gonna see if I can
get the ICAO out of there,
which I should be able to do.
Then I'm going to look this airport up
by calling my own withICAO
function in this context,
'cause this is happening
sometime later, right?
This closure happened sometime later,
so now I'm gonna go back
and call this again,
to get this Airport that
I created earlier actually
the one that just has the
ICAO and I'm gonna go back
and get it 'cause this
is all happening later.
And then I'm just going
to set all the fields.
The latitude =
info.latitude, and longitude,
longitude, you gotta
type all these fields in.
Next step I wanna take here
is to save this information
into the database.
Now that can be done by asking
the context to do a save.
But you can see here that
the context save throws,
see where it says throws right there.
Anytime you see throws,
that means that this is a
function you have to try.
And I'm happy to try this.
And if it fails, just do nothing.
Now this is not all the
vars on my Airport though.
If I go back and look at my Airport,
I've got all these vars,
but, what about these?
flightsFrom a flightsTo?
Well flightsFrom and flightsTo,
can be set from either side,
you can either set it by having
a Flight set its destination
to an Airport, and that's going
to cause the flightsTo Set,
to get an Airport added
to it automatically.
You don't have to do anything.
And vice versa.
If you add something to this
flightsTo, on this side,
it's going to cause that Flight
to point back the other way.
So that's pretty cool.
That's automatically managed for us.
Now what about one other
thing we wanna do here.
So we've created this thing.
We saved it.
Remember that these Airports
are ObservableObjects.
They're a little mini ViewModels.
So I'm doing a wholesale
update right here.
I'm going to actually tell
this Airport to fire off its
objectWillChange.send().
That's gonna cause any Views
that are looking at this
Airport right now to redraw
themselves and hopefully pick up
this new information if
they depend on any of it.
Hopefully we can do another thing too.
I'm gonna have the Airport's flightsTo,
I'm gonna have each of
them forEach do their
objectWillChange.send().
Lemme do the same thing
for my flightsFrom.
Now, this doesn't work because
of what I was saying before
that these flightsTo and
flightFrom our NSSets,
you see that, the value of type NSSet.
So you can't tell an NSSet
Optional here to do this.
And even if I make it non-Optional,
that's not gonna fix the problem either,
because it's not only an
NSSet, it's an NSSet of Any.
So it doesn't even know
that these flightsTo,
are actually Flights, Flight objects.
So this solution here requires
a little bit of what I call
syntactic sugar not added by the compiler,
but something we're gonna add.
I'm going to make
flightsTo and flightsFrom,
be little computed var, flightsTo,
I'm gonna have a Set of Flights
and I'm gonna have a get and
a set and the same thing with flightsFrom.
Now, how am I gonna do
this flightsFrom and To
as computed vars because
I already have flightsTo
and flightsFrom vars from over here,
and my airport already has
these var, these are vars.
So I'm gonna rename these vars over here,
to put a little underbar after them.
And this is just something kind of,
I like to do when I have things
that are set right here and
I'm gonna do it for other things later,
where the thing in the
database is almost exactly
what I want, but not quite.
So this is a set of these
Any from kind of Objective-C
version of this thing.
And I'm gonna have my
flightsTo get by returning the
flightsTo_ as a Set of Flight.
And oh, by the way, if that is not set,
I'm gonna return an empty Set of Flight.
So here, I've kind of
done two things at once.
I've converted flightsTo, to be,
not be an NSSet and
instead be a Set of Flight,
and I've also checked to see
a flightsTo might be nil,
and if it is returned
an empty Set instead.
So setting it is easy as well.
flightsTo_ = newValue as NSSet.
So the Objective-C versions
of Set and the Swift version
Sets can be as-ed, this is
typecasting, and hopefully you
remember from reading again,
they can be as-ed directly as each other,
which is really a cool feature,
very big compatibility
feature to have that work.
So my flightsFrom is pretty
much exactly the same thing.
We're doing flightsFrom here,
and flightsFrom.
That's it, everybody hopefully understand,
I'm just trying to make my
code here look really clean
and sweet by making it be Set
of Flight instead of being
this NSSet of Any and
possibly nil as well.
So that's all I think I need
to do here to create an Airport
or find one that's existing.
So if I go back over to
my SceneDelegate here,
oh, look at that, all
that code just compiles
perfectly fine here.
Now this thing that I
did with Airport here,
with flightsTo, again, not only
switching from Set to NSSet
but also checking to see if it's nil.
So this flightsTo at worst
is ever an empty Set,
not nil.
I like to do that with
all my other vars too,
that can't really be nil.
For example, let's go
look in Airport here,
and see which of these can
never, or should never be nil.
Well, actually, any of these can be nil,
and they're all gonna be nil
while I'm doing my fetch,
except for not this one, icao
never really should be nil.
So I'm gonna put an underbar after it.
And then in my extension over here,
and I actually have
this commented out here
so that we can speed this along.
I'm going to create a similar sort of var,
this one's not doing the Set NSSet thing,
but it's just making this
thing be not Optional anymore.
So it's taking the underbar
version, make it not Optional.
Now I have a to do here you
see that when my app ships,
maybe I wanna do something else here,
when this thing is nil
because it's really an error condition.
This should never happen because
when I create an Airport,
the very first thing
I do on the next line,
is set this icao this
can really never happen
that this is nil.
If this did, it's some
error condition going on.
So I could possibly try and
handle this error condition
better than just crashing
because we know that exclamation
point is just gonna crash.
But certainly for this demo,
and while I'm in development,
maybe I want this to crash
while I'm in development.
I wanna see the cases under
which I can get this kind of
corrupted database to happen,
if ever, may never happen
that this can happen.
I've also added some
other things down here,
a friendly name for the Airport,
just by looking at it's name
and location carrying that.
I've also made Airports
Identifiable and Comparable,
it's usually a nice thing to
do with your objects from Core
Data so you can put them in dictionaries
and Sets and things like that.
So we've done a lot of great
stuff here with Airport,
and we've learned a
lot about the Core Data
just from doing Airport.
We know how to fetch them, we
know how to create new ones,
we know how to do this little
thing where we make our sets
look a little nicer, we know
how to kind of cover our
can be Optional ones.
And in fact, let's do this
little can-be-Optional cover
for our other things as well.
We have Airline over here.
So I've done the same thing
here, now for Airline,
I want code, name, shortname,
and then of course,
we're gonna do the flights Set magic here.
So code, name, short name,
flights, that's pretty much all
the things down here in Airline,
to make that an underbar, name,
underbar and shortname,
underbar and flights also here,
it's underbar, and how about Flight?
So I do the same thing for Flights,
which we haven't even looked at Flight.
All right.
So Flight, I made the arrival,
the destination, origin, airport,
the airline and the ident,
all of these things, can't be nil, and
then I added this nice
little number thing.
Let's go over here to
our data Model for Flight
and aircraft is fine, arrival,
make that arrival time can't be nil.
departure time can be
nil, filed could be nil,
ident cannot be nil.
And then all these relationships
we're going to just protect
them against being nil.
So that's all we've done there.
This just makes our code nicer.
We don't have to be constantly
checking to see if the ident
is not nil because a Flight's ident
really can never copy nil.
If the Flight didn't have an ident,
it doesn't even exist really.
And same thing arrival time,
Flights have to have at least
an estimated arrival time.
Now, one thing to be careful of
when you go through and do
this kind of getting rid of the
Optional mess of things,
is that your fetches,
like this fetch right here,
it still has to use the underbar version,
it can't not use an
underbar version for that.
Anything you make not have the underbar,
this request right here, this predicate,
it's doing fetching on
fields in the database,
not vars in your code down here.
It's actually fetching in the database.
So this has to be a field in the database.
Same thing with SortDescriptors.
Now the location, if we
look back here at Airport,
this location, we didn't underbar that.
So we don't want the underbar there,
means we don't want the
underbar here either.
So we fixed all of that problem
that we originally had here
in SceneDelegate, where we
were trying to create a Flight
search with KSFO instead, we
just made ourselves an Airport
object and pass that in
so that our FlightSearch
was now taking a destination
that was an Airport.
Let's continue to chase after
our bugs over here that we
introduced by making this
all be object-oriented.
This one says, binary
operator == between String and
Airport of course.
Another one of these is where's that,
that's in FlightFetcher,
so I'm clicking on that,
so this is part of the
private implementation
of FlightFetcher.
Yeah, of course, FlightFetcher
is all non-object-oriented
based on Strings and fetching Strings.
So FlightFetcher itself is just gonna be
completely useless.
So let's go back to our
FlightEnrouteView and go
look for FlightFetcher.
Here's where we use it in our Flight list.
Fetch Flights, obviously
for our Flight list,
this whole thing is useless.
I'm just gonna delete all
this FlightFetcher code
because we can't use any of it anymore.
So how do we do the equivalent though?
In Core Data, we want
to go fetch all of those
Flights and ForEach through them.
Well, it's pretty cool
integration here between Core Data
and SwiftUI, I'm actually
gonna create a var
for those Flights right here.
And I'm gonna do that var
using a property wrapper
called @FetchRequest.
And it takes some arguments
which we'll talk about in a second.
And this return value of
something that is using this
property wrapper is
called a FetchedResults,
many times it don't care,
which is the kind of thing
it's fetching, which is Flight.
So this Flight, remember
that's our Flight that we're
creating here from our database.
And we're gonna issue
some sort of FetchRequest
to get some flights.
And this var is going to
be this FetchedResults.
Now, what is a FetchedResults of Flight?
It's not quite an Array of Flights,
although it is a Collection.
So it'll work to put it right here,
and for each it's worth just mine,
because this is essentially a Collection.
And that's what ForEach is looking for,
a Collection of Identifiable
objects basically.
This is essentially the result
of this FetchRequest as a
bunch of Flights, which
is exactly what we want.
Now, how do we specify
this FetchRequest here?
We essentially have to give it
the exact same information we
did over here in Airport, each
one of these FetchRequests.
So we have to give it a predicate,
what this entity name is,
what the SortDescriptors,
and you can do that,
there is something I think it's called,
yeah, here it is entity you
would say something like
Flight.entity() and then
there are SortDescriptors,
you would put those.
And then there's also
another one predicate,
and you put those,
but we're not gonna do that
because we really can't.
The problem is, what are
we trying to fetch here?
We're trying to fetch things
that match this flight search.
So unfortunately this flight
search is passed to us,
we can't use it in here to initialize this
property, wrappers struct.
So we're gonna have to do it in here.
And we know exactly how to
initialize property wrapper
structs, we use the
underbar version of it.
So _flights,
there's the under bar
version of this Flight,
that's this struct, I'm
gonna set that equal to
a FetchRequest that's one of
these structs that I'm gonna
create with a FetchRequest.
So this is one of the
constructors for creating a
FetchRequest, I told you,
there was the other one here,
that took the entity and all that.
But one of the choices
here also is FetchRequest.
So I'm just going to
give it a FetchRequest,
and this FetchRequest I'm
gonna do exactly the same thing
I did over here when we
did the Airport look up,
lemme get this document and
copy and paste it, so similar.
And here this time, we're
not looking up Airports,
we're looking up Flights.
So here we're not looking
up the ICAO, of course,
we're looking at the destination
airport of this thing,
which we underbar-ed because
it doesn't make sense to have a
Flight without a destination.
So that's why we made it
so that it would never be
Optional there.
And what is the destination?
It's the FlightSearch
that we're being passed,
its destination, this FlightSearch,
it's a FlightSearch
object, we changed that,
so its destination's an Airport.
So now we are doing a fetch
here where we're looking for
destination equals that Airport.
Now we have other SortDescriptor obviously
let's sort by, I think our arrival time
is probably the best thing
to sort our Flights by.
Now we have other predicate
things we wanna search for
in here besides the destination,
like origin Airline,
inTheAir but we'll do that a little bit
when we get our FilterFlights working.
So for now we're just going
to look for all the Flights
to this destination.
Now you see that I've typed
the three lines of code here,
I typed the same three
lines of code over here.
They're very, very similar.
You really kind of use them all the time,
'cause you're searching
for objects all the time.
I like to make little
functions in my extensions
to these that do these
three lines of code for me.
So instead of doing these
three lines to code,
I'm gonna use some code that
I put down here to do this.
And it just makes a
FetchRequest with a predicate
and it automatically picks
the right SortDescriptor.
It does this little
entityName thing to create the
FetchRequest in the first place.
And then it just plops the
predicate that you want in there.
And that changes this three
lines of code up here to a
single line of code, let
request equal a fetchRequest
where the predicate is NSPredicate,
and our predicate here was "icao_ = %@"
of this ICAO.
So now I don't have to
do the SortDescriptors
and all that stuff.
It just makes this code simpler.
And let me do the same thing for Flights.
There is FetchRequest for
Flight, sorts by arrival.
It's got the right entity, name here,
I'm gonna do the same thing for Airline.
And I pretty much will do this
for all of my objects that
come out of Core Data to
have these easy one-liners to
create a FetchRequest for them,
especially when you're
almost always wanting them
sorted by the same thing,
and Airline almost always
gonna be wanna be sorted by
it's name, and Airport
almost always gonna
wanna be sorted by it's
location down here.
Flight almost always gonna be wanna be
sorted by arrival time.
Now, if you wanted to specify
different SortDescriptors,
of course you could get the
FetchRequest in here and then
change the SortDescriptors,
it's just returning a var here,
a FetchRequest var.
So that makes our code
back here even simpler.
We can actually just take this
predicate right here and say,
let request equal a Flight.fetchRequest,
where this is the predicate,
so that, makes this a one-liner here.
So now this flights var
is going to always be
the result of this fetch.
This is the amazing thing about this
property wrapper right here.
It's more than just letting
you specify the FetchRequest.
It makes it so that this var
always contains the result of this fetch.
Even if objects are being
added or removing from the
database that would match this,
it always is changing this.
This is just constantly being updated,
this fetch results to
reflect the latest version.
And it's really one of the
really best integrations
between SwiftUI and Core Data,
is that this ability to make
these fetch results vars that are just
always kept up to date.
So for each down here,
it's just always going to be
updating every time new Flights
come in from FlightAware or whatever,
it just automatically updates.
So let's keep going here.
What's the next error,
we can just see it right here actually.
FlightListEntry flight of flight,
cannot convert FetchedResults
of Flight Elements.
So the Element of a
FetchedResult of Flight,
which will be AKA, a Flight,
to expect an argument,
FAFlight, of course,
because a FlightListEntry,
this thing down here, it
expects to get an FAFlight.
'Cause this is the old world here,
the pre-Core Data world.
So let's fix this.
This flight does not
want to be an FAFlight,
it wants to be a Flight.
And one huge advantage of that is
that flight is an ObservableObject.
So I'm gonna say @ObservedObject Flight.
So if anything changes about
this Flight in the database,
this View is gonna redraw.
This is one of the huge
advantages of making this object-
oriented, instead of
having this be a String,
this is a String, there's no way to know,
oh, that Flight got updated.
And in FlightListEntry we
also have these other objects
which are getting all the
Airports and all the Airlines.
We don't really need
those anymore because it,
when we get Airlines and Airports
nowadays, they're objects
and we can get all the info
we want about them from those
objects, so we don't need to
have these allAirlines flight
code, friendly name business,
instead, I can just go here
and say, flight.airline.friendlyName.
And similarly down here,
we don't need this allAirport
to look up the flight by its
origin whatever, we can just say,
flight.origin.friendlyName.
So this is all much more
object-oriented and it makes the
code a lot cleaner, like
friendlyName, flight.number
and friendlyName
down here in the "from" field,
less of looking up things in
random lists of info
structs and all that stuff.
We still have all these
things in FlightFetcher, okay,
poor FlightFetcher.
Let's go over to FlightFetcher
here from the last
lecture and just comment
this whole thing out
because we really don't
need FlightFetcher anymore.
Because we're using now just our database.
There we go, and oh, no more errors.
We fixed everything, changed
our world to be entire object-
oriented and pretty straightforward
to fix all the things.
And in fact, some of this
code much more readable
and understandable.
Let's see if it works.
And there it is, and it does not work.
Why?
It's just no information,
but this is actually working perfectly.
It's just that there's
no data in our database.
So we are looking at an empty database.
Here's all the Flights
in our empty database.
So we clearly need some code
to load up this database
with the Flights to the
destination airport.
So let's do that, over
here, there's new data,
here's where we create this SFO.
Lets create a little
Airport function here called
fetchIncomingFlights.
This is just gonna go off to FlightAware,
fetch all the flights they're
coming to this airport
right now, and put them in the database.
And then everything will
just work from there
because the rest of our
code is just looking in the
database, just waiting
for something to happen.
This fetch right here, is
just sitting there waiting for
this to start giving some results.
And it's just gonna redraw
and automatically do it.
So let's go to Airport here,
and do this fetchIncomingFlights.
Now the fetchIncomingFlights,
I also commented it out,
it's mostly again this FlightAware stuff,
but I do wanna focus on
this line right here.
This context = managedObjectContext.
You notice that when we
created this function
fetchIncomingFlights,
we didn't pass the context,
the database context
as an argument.
So you might wonder how are
we gonna create Flights,
we're fetching incoming flights,
how are we gonna create
them in the database
if we don't have a context?
Well, this method
fetchIncomingFlights is an instance
method on an Airport, and it
turns out that all objects
that come out of Core
Data, Airports, Airlines,
Flights, any of them,
they know the context they came out of.
And we're just gonna
use that same context,
to put our Flights in.
So this line of code, if let
context = managedObjectContext,
is just accessing a var in the Airport,
which is, what is the
managedObjectContext,
you came out of.
And this is really important
to know because otherwise
you're gonna be end up
passing context around all the
time unnecessarily, when
you have an instance
from the database in your hand,
you can always get the context
from the database and add
more objects or fetch
other objects using that.
Here's the asynchronous closure
that's executed when the
information comes back from FlightAware.
So this is very similar
to what we had up here,
when this AirportInfoRequest
fetched an airport,
and here's where it came
back with the result.
We have the exact same
thing going down here,
where we have the result.
So what am I gonna do when the information
comes back from FlightAware?
Well, this is an Array of FAFlight,
you all remember FAFlight
over here as all the
information that comes
back from FlightAware.
So I'm going to go through
all those FAFlights
with a for loop, for
faflight in those results.
Now I'm gonna call a function in Flight
that updates from an FAFlight.
Again, just like I have
update from up here,
that updates from AirportInfo in Airport,
I'm gonna put this method
almost exactly the same method,
but with all flight-oriented
stuff here into Flight.
So let's go over and do that.
Here's Flight, and you can
see it's doing the same thing,
FetchRequest, looking things
up by ident all these things.
This again, could be, let
request = fetchRequest,
with this predicate, so we can make that
a one liner right there.
And so we're searching for
this Flight by whatever it's
ident is in the FAFlight.
That's what the search predicate is.
And we're doing exact same
thing we did in Airport,
looking to see if we found a result,
if we find it, in this case though,
Flights are a little
different because they get
updated over time.
So if we find it or not,
we're going to update it with
whatever information came back.
And this is all just stuff we saw before.
A couple of interesting
things going on in here
is here I'm setting
some relationship vars.
So here's my origin and
destination airport.
Look what I'm doing,
I'm creating an Airport
with that origin var
'cause an FAFlight origin is a String.
So I'm looking that ICAO
up in our current context,
the context that we've
passed into us here,
and creating an Airport.
Same thing here for the
destination Airport.
And after the same thing for
airline, because in FAFlight,
airlineCode is a String.
So I need to create an Airline.
So in Airline we need a
little thing that looks it up,
and finds it, return to
it, if not, it creates it
just like we had an Airport.
So let's go do that in Airline,
and this with code, looks
very similar to the Airport.
It's just that these are
all different down here.
We're still doing the
objectWillChange though here.
And same thing with the Flights.
So if this airline
information comes available
from FlightAware, we're
going to do objectWillChange
on all those things.
One of the things I
wanted to show you is the,
when I'm loading up these Flights,
I'm going to show you what it
looks like to try and catch
the error from context save.
Just because we haven't
really been doing this,
we've been doing try? everywhere,
but if you just put a do catch
around the thing you wanna
try, this throws an error and
this will catch that error.
And then you can do things
like print out that error's
localizedDescription or
something right here,
and here we can handle the
error where we try to save the
context and we couldn't.
If your context save fails
something pretty bad going on,
maybe your disk is full or, you know,
something pretty heinous
is happening here.
So there's not usually much
you can do to handle failure to
say, maybe try to save again
in a few seconds or something,
but it's a tough one to recover from.
So that's all that's happening
here in fetch incoming
flights, we are just loading
up the database with things
that are coming in from FlightAware.
That's all we're doing repeatedly.
See if that works.
Well, look at that, loading
it up in the database.
And wow, now our data is
showing a lot of Flights
and they're still coming in.
It's loading more and more flights.
And what we're looking at
here though, is the database.
This is purely looking at the database.
All this flight fetching that's happening,
happening asynchronously to
what's going on here in the UI.
Now glancing at my code there on the left,
I can actually see, we forgot something,
which is we don't have our
Flight do its objectWillChange,
even though we're possibly
doing a big update
to a Flight there.
And then also I see a
problem here at the top
where it's not saying flights to KSFO,
it's saying flights to
airport 0X, whatever.
And that's happening because
we're printing out an Airport
object there, instead of
printing out it's ICAO code.
So let's fix both of those problems,
starting with right here,
we forgot to say that we want our Flight
that we just created, to have
its objectWillChange.send()
sent because we just changed
a lot of fields and we wanna
make sure that our UI will
update with whatever that is.
And then we can fix that title
problem with the Airport,
which we're doing over here,
by saying backslash open
parentheses, closed
parentheses, with the object.
We don't really want the object here.
We want this destination's ICAO.
So let's do that.
Fix that problem.
There it is.
There's it's ICAO.
All right.
The last piece we have to do
is fix our filter right here
because our filter, lemme show you
what they'll do right now.
It's not gonna work because this filter
is built to work with Strings.
It's supposed to be
picking Strings over here,
this FilterFFlights.
And you know, we changed all
of our Flights to be objects.
So this needs the object-oriented
treatment as well over here.
Now, when we're letting people
choose Airports and Airlines,
we actually do need all the
Airports and all the Airlines
that are available,
but we're not gonna do it
again with these old ViewModel
airports all and airlines all instead,
we're gonna do FetchRequests here,
and these are gonna be
FetchRequests for Airports and
Airlines, I'm gonna do a FetchRequest,
and I'm gonna have the
FetchRequest here be hardwired in
here, it's gonna be airport.fetchRequest,
where the predicate is essentially all,
I need all my Airports.
I'm gonna use this FetchRequest
to get a little var
airports here, which is
gonna be all of my Airports.
This is the same FetchedResults
thing that I was doing over
here, but over here, I was doing
the FetchedResults based on
looking up the destination
Airport, in this case,
I want all of my Airports.
So what kind of predicate means all,
well, there's a great predicate for that
called TRUEPREDICATE.
That just basically means this evaluates
to true at all times.
That's so common to use.
I actually created a little extension,
called, to NSPredicate,
called, all which does that,
and none which does the false.
Seems like I'm always doing a predicate,
TRUEPREDICATE somewhere in my app.
So I'm gonna change that to all.
That's it, that's going
to fetch all the Airports.
This airports var will
always be all the Airports,
even if airports are added,
this automatically updates so
that this FetchedResults thing
that we're doing ForEach-ing
inside of our Pickers down here,
will always be updated.
And same thing we'll do for our Airline.
Now we have all our Airports
and all our Airlines,
and we're gonna use that down
here, inside of our Pickers,
because our Pickers are
currently picking these codes,
but we want them to
actually pick the objects.
We want them to pick
Airport and Airline objects.
So instead of being
all airport codes here,
this is just going to be our Airports,
and I'm gonna sort them, sorted.
Our Airports if you remember,
when I showed you Airport
over here, I made it so
that it implemented this
Comparable protocol.
So this Comparable protocol is
a little less than function.
And if you implement Comparable,
then you can have Collections
like Airports or Arrays of
things, sort themselves by saying sorted.
Now this is object-oriented.
So we don't wanna be, need to
be looking this up like this.
We can just say, instead, here
our Airport's friendlyName
to get the Airport, and
this never returns nil,
so we don't need to
optionally default that.
Good, so this code actually
cleaned up quite nicely.
And what about our origin?
Similar, except for that, of course,
this is no longer a String.
So we don't want the Optional
nil for Optional String.
We want Airport Optional,
right there so that's
been nil for Airport.
And again, not allAirports.codes,
we want Airports.sorted().
And again, the Airport is already
got its own friendlyName in an
object-oriented kind of way.
Still want the ?? Any, because,
this airport is allowed to
be nil and it's not Optional
String, it's Optional Airport.
So now this is picking Optional Airports,
just like this is picking Airport.
And this is Airline down here.
This is probably more similar to this,
than it is to this other one.
So if I copy and paste right here,
change this to be our Airline.
And I'm gonna search and
replace this airport,
with our airline
We've converted these now to use objects
instead of their Strings.
Otherwise this code can stay the same.
One last thing that I wanna do is,
when the done button is pressed,
if you've changed your destination,
I wanna do this, fetchIncomingFlights.
If you change from looking at SFO,
to looking at Dallas Fort worth,
I want to fetch those incoming flights.
So I only wanna do it though,
when you actually change your destination.
So I'm gonna say, if my draft's
destination does not equal
my flightSearch
destination, then self.draft
destination fetchIncomingFlights.
Lets take a look.
See if this works.
All right, here we go.
Let's try filter.
Oh no, it crashed, it
crashes, we hate crashes.
What is going on?
Let's look at our console down here.
This is a very, very important error
when you're working with Core Data,
to understand what this is.
It says, context in your
Environment, is not connected to
the persistent store coordinator.
If you remember back over
here in SceneDelegate,
we created this thing context
out of our persistent store,
this persistent container.
This is our View into the database,
and we had to pass it
in to our Enroute View,
via this environment.
But we then, from our honorary View,
put up a sheet with
FilterFlights and sheets
get their own environments.
So we have to do the same thing here,
and pass into this
environment, this context.
Now the only problem
here is, what is context?
It says unresolved
identifier, where is context?
Well, we're gonna have
to get our context here,
so that we can pass it here.
We're gonna get that here,
from our environment,
a managedObjectContext, which
is a var we'll call context.
So here, I'm grabbing
this context out of here.
I'm out of my Environment,
and I'm passing it along
to this sheet's Environment.
So this guy has Environment.
So this is a common
error to get down here.
And so, really make sure
that you burn in your brain,
what this means.
It just means you needed to
pass into a sheet or something.
Anything that puts up this
modal popover whatever,
pass this context in there.
And the reason for that is,
things like FetchRequest
depend on that context.
How else are they gonna know
where to get these Airports
and Airlines from?
They need the context to
be in their Environment.
Oh yes.
So, we gotta put a self dot over there,
now we can do it.
Okay, here we go, let's try it filter.
Oh, that looks better,
destination San Francisco.
Wow, there's are all over the Denver.
Now that didn't fetch, that's good.
I haven't said done here,
so it didn't go off and do a fetch there.
Origin yeah, looking good, airline yeah,
I'm gonna say United.
What about that?
Oh, airline not working.
See that airline, not working.
So let's go back and take
a look at airline tell you,
what we did wrong there
probably a copy and paste error
that we made down there.
So that's in FilterFlights.
So airlines see, oh,
look at this draft.origin
for the Airline, no,
this is draft.airline.
Try that.
Alright, so that's it for, that's good.
Now airline, oh, looking
much better already,
United, lets go to Boston,
and I'm gonna hit done,
and hopefully then it
will do another fetch.
Whoop, there it is Boston.
And again, we're looking
in the database here,
and if we go back to San Francisco,
you'll notice something interesting here.
Pay close attention.
Well, a lot of airports in there.
I think it's just go, here it is.
Watch when I hit done here,
whoop, how quickly SFO load it up,
because it's showing me
stuff that's in the database,
and it's continuing to update them,
but we're getting this instant
response from the database,
which is another great kind
of side effect of moving this
stuff in the database.
Now it's not quite right
yet, because I changed this
if you remember, to have
United as my airline,
but this is giving me other
airlines besides United, right?
Well that's because back
here in our FlightEnroute,
when we were looking up our Flights,
our predicate here is
just the destination only.
This is not looking at the
airline and things like that.
It's the destination only.
So we need this predicate right
here, to be some predicate,
say that predicate equal, date
is based on this FlightSearch
and all the things, not
just the destination.
So I put a little extension
down here to FlightSearch
to do that.
And let's just move it all the way
up to the top of the file here.
And this FlightSearch extension
that I added is to add a
var called predicate.
That just is our implied search, right?
So it's just looking at all these things,
and building up an NSPredicate object,
using more and more stuff in its format,
you see its format is destination.
Then if there's an
origin, I add, and origin.
If there's an airline, I add, and airline.
And along the way, I have this args,
which is an Array of NSManagedObject.
Now, what is NSManagedObject?
This is the superclass of
Flight, Airport and Airline.
So if I wanna create an Array
that could have Flights in it,
or Airports or Airlines,
then I can make an Array
of NSManagedObject,
since those all inherit
from there, this works.
So this is the args, so
it's getting the destination
in there to start, and
then if there is an origin
it puts that in there, if there's
an Airline it puts that in
there, and then we're just
saying NSPredicate format,
this String we built up,
and the argument Array,
is these args that I build up.
This is how you kind of, can
kind of build a programmatic
predicate that's based on decision-making,
like what's in this struct.
So I'm just gonna use this
predicate var down here,
and say, flightSearch.predicate.
Give me a predicate for this FlightSearch.
And I'm gonna use that to search.
We don't need that, not anymore.
It's simpler, in fact, this is so simple,
we could just put this
right in here like that.
See if that works.
Notice SFO instantly showing
us data in the database,
the data that's valid anyway, to show us,
and let's go airport here,
and go down to United,
that's a hub for San Francisco.
So there should be flights and churn up.
There it is, that's working
and we could go origin.
What was in the origins there?
Boston, we can find Boston.
We know that Boston is there,
Logan airport in Boston.
So now we're gonna look
both Boston and United,
found that, that's good.
And, we should go back to
saying, show us any plane
coming from Boston, on this one flight,
lemme go back here and
say, shows any airlines,
any place, here they are.
Alright.
So that's a lot of code in that demo.
Lot to show you there,
but hopefully you've gotten a
good feel for how we build our
UI out of data in the database,
especially the way we do
these FetchRequests that are
recurring results, they're
just constantly kept up to date
as we change things.
And that makes it to
the rest of your code.
Doesn't have to have many
if thens, and things change,
and this, that, and the other thing,
it just finds them.
And notice that in Core Data, we're
mostly using these FetchedResults
to have things change.
Even though we can also use
ObservedObjects on individual
Flights, Airlines, right, and
we're using it here if this
Flight did change, since we did the
objectWillChange.send() on there.
It would update,
but that's more rare even than
doing these FetchedResults,
really FetchedResults and
having this dynamically always
show you what's in the database,
that's really the heart of the SwiftUI
to Core Data integration.
All right.
I will really love to see some of you
do this in your final project.
There's certainly a lot of
need in a lot of different
project ideas, for permanent
storage resistant storage.
And this is a lot more powerful
than doing UserDefaults.
So I'm hoping that you
guys will tackle this.
- [Animaker Voice] For more,
please visit us @stanford.edu.
