(serene music)
- [Narrator] Stanford University.
- Hello, everyone.
Welcome to one CS193p, spring of 2020.
This is lecture 11, al fresco version.
Here outside to celebrate that
we pretty much covered the
main topics of SwiftUI.
And you should be able now to go off
and do your final project
after you've finished with assignment six,
which you're in the middle
of working on right now.
I'm still gonna have a few more lectures,
although I will not be having
lectures during dead week,
nor am I gonna do a
lecture on Memorial day.
So I believe that leaves us
with three lectures more.
I will try to accelerate those
lectures as much as possible
because they're all gonna be topics
that you might wanna work
on for your final project,
not required, but you
might wanna use them.
And if I wait too long to bring them out,
you won't have time to incorporate 'em
into your final project.
So I'll try to get those out
as soon as I possibly can.
And speaking of your final project,
read the rubric carefully.
And if you have any questions about it,
make sure you ask in the
class forums, questions,
I'm happy to answer and
clarifications, et cetera.
In terms of advice for your final project,
I really recommend doing a
good detailed project proposal.
That's your first deliverable.
It's due on Monday
and we are not gonna be held responsible
if you say you're gonna do
something in that proposal
and you don't end up doing it.
So it's better to suggest
a little too much there
and then come up a little short,
than the opposite,
suggest not quite enough,
you're not sure what you're gonna do
and then you dive into it
and you really haven't thought ahead
and planned out what you're gonna do.
So that's my first piece of advice.
My second piece of advice,
old hackneyed advice
from all your instructors
of your courses here,
which is start early.
Now, this is not just start
early so you have more time,
you wanna start early
because as I've said,
maybe even said in this
course, I don't remember,
but programming is like
doing a crossword puzzle.
Even if you're really
good at crossword puzzles,
you'll sometimes get started on one
and you just can't figure it out.
And you're just convinced
there's just no way
I'm gonna get these last four
across and down in the corner
or something like that, it's impossible.
And then you go to sleep
and you wake up next day
and you have your lunch.
And then in the afternoon,
all of a sudden you're like,
oh yeah, I know what 39 down is.
And once you get 39 down, the
whole thing falls into place.
And that's because your subconscious
got a chance to work on
that crossword puzzle.
Your conscious mind is distracted
by a lot of things going on around you.
It's got limited bandwidth.
Your subconscious mind can
sit there and cook on problems
and just come up with the
most amazing solutions.
And so let your subconscious work.
Work on your final project,
even if it's just 20 minutes in a day
to touch base with it
and remind your subconscious
mind what's going on.
You have a really good chance
that the next day you're
gonna get 39 down, okay?
Or the equivalent thereof in programming.
So I really recommend that.
All right, big demo again today,
covering another topic
that's not required
for your final project,
but it's likely most of you
are actually going to
want to use this thing
called a Picker.
So let's jump into that right now.
This demo is mostly about Picker,
but we're going to do it in
the context of a new app,
not Memorize or Emoji Art
and this app, which is called Enroute,
and I'm gonna show it to you in a moment
is also gonna be the basis
for next week's demos.
So today we're not just
learning about Picker,
we're learning a little bit
about how Enroute works,
so when we do our stuff next week,
we'll have a shared knowledge of it.
Another difference today is
with Emoji Art and Memorize
we started those
applications from scratch,
but this one I'm starting
with a working code
that does certain amount
of functionality already,
and we're gonna add functionality to it.
And I'm doing that because
this application fetches some
information from the internet
and so the code to do that
is not something that
really is within the scope
of this class.
You're certainly welcome to look at it,
see if you can understand
it, what it's doing.
It's not that complex and it's
all pretty much demo-ware,
just something I put
together over the weekend
for this demo.
But I don't really wanna
go through that in detail.
Instead, I wanna focus on some features
that will really help you
with your final project
like today, Picker.
So let's review this app called Enroute.
I'm gonna run it so you
can see what it looks like.
Enroute essentially uses an API
that's available on the internet
from a company called FlightAware,
and you can see that
it's actually loading up
some flight information here.
In this case, it's loading up
flights that are in the air
enroute to San Francisco
International KSFO.
That's its little airport code there.
And you can see that it's
telling us this SkyWest flight
arrives today at 11:42,
that's in two minutes,
it's coming from Los Angeles.
And it's telling us
about all these flights,
different airlines, different
origin airports, et cetera.
And our goal today with Picker
is we wanna add some UI.
I'm gonna put a little
Button in the corner here
called filter
And it's gonna bring up a modal sheet
that lets us filter these results
by for example, where it's coming from
or which airline or whether
it's in the air or not,
because there are flights
coming to San Francisco
that are scheduled to
arrive at certain times,
but maybe they haven't taken off.
They might be scheduled to take off later,
or they might be delayed or whatever.
I'm not currently showing any of those.
All these flights you see
right here are in the air,
actually flying to San
Francisco at this very moment.
And this is real data
coming from FlightAware.
FlightAware is not a free API to use.
It's quite inexpensive.
So hey, if you have a few bucks,
you can head over to FlightAware
and sign up and get your own key.
And so then when you run this demo code,
you can get live data like
I'm getting right here.
Or I also am gonna provide
some simulated data
so that those of you
who don't wanna go to
FlightAware and sign up
you'll get some simulated data
from over the weekend basically.
It only has a couple of airports,
San Francisco and Las Vegas, I think,
but it'll let you at least see the code
and get your Picker working
and things like that.
Let's talk a little bit
about how Enroute is built,
how this app works.
So this View right here
that you're seeing,
this list of flights
is just a single View.
It's the only View file that we have
in the whole app right here,
this FlightsEnrouteView.swift.
And so let's go through this.
You can see it's actually
quite a simple little View,
really not much to it here.
At the top level there's
this FlightsEnrouteView.
It's just a NavigationView.
You did notice that our app over here
is definitely in a NavigationView,
even though we don't click
on these and navigate,
one day maybe we would, but we don't now.
You're still getting the
title of NavigationView
and if we had some buttons up here,
like we're gonna put that
filter button up there
we are getting the benefits
of being a NavigationView here.
And then inside that
navigation is this FlightList.
This FlightList is this View right here.
Another really simple little View.
All it does is have a List
that's ForEach through all the flights
and it uses this FlightListEntry View
to show each of the
things in the flight here.
So this is the FlightListEntry
is showing you all this stuff in each row.
So let's take a look at FlightListEntry,
which is this View right here.
It's also simple little View.
It's just a VStack with
the name of the flight,
like United 245, when it arrives
and where it's coming from.
And that's it.
That's the entirety of FlightListEntry.
All that's happening down here
is where it's calculating
Strings for all those things,
the name, the arrival and the origin.
Now, one thing that's
interesting about FlightListEntry
is it has two ViewModels here.
Each of these ViewModels represent
either all the airports
or all the airlines
that my app knows about.
So as I'm using my app,
it's finding out about more
and more airports and airlines,
and it's asking FlightAware about them.
What's the name of them and where are they
and things like that.
And that information is all coming back.
And these are just ObservableObjects
that when the information changes,
they do, their objectWillChange.send()
and they update and cause
things like this to redraw
with the proper information
of the name of the airline,
or the name of the airport.
So that's all that's going
on here in FlightListEntry.
So this is a View like you're used to
with a ViewModel, has two ViewModels.
It's perfectly fine for a View
to have multiple ViewModels
as we'll see next week.
And it just takes any flight.
One of these FAFlights
and this FAFlight comes
back from FlightAware.
We can take a look at that.
Here's all the code for
FlightAware in here.
You can go look at it later.
But an FAFlight just has things
like the identity of the flight
and what aircraft is being used,
the destination and origin cities,
has some little functions.
Notice that I made FAFlight
to be Codeable and Hashable
and Identifiable and Comparable
and CustomStringConvertible.
So there's a lot of things I made it be.
You know what these things are.
Comparable is an interesting one.
This is the Comparable
protocol right here.
It just lets you compare two things
to see if they're less than.
This is a nice one to implement.
I'm comparing them by arrival dates,
which is how we usually
wanna sort flights.
And one thing that's really nice
about implementing Comparable
is that the Array method sorted
will work on an Array of FAFlights here
without any arguments.
And it's just gonna use
Comparable to compare the flights.
And then this other one up
here, CustomStringConvertible.
Also a very interesting one to implement.
That is this var description.
If you implement
CustomStringConvertible up here
and you provide this var description,
then you can say what happens
when this object, a flight object,
gets put into a String with
backslash, open parentheses
closed parentheses, right?
Normally that shows something
that Swift generates about it,
but you can have it say something
that is more easy for you to understand.
Here I just identify my
flight with identifier
and its departure and arrival
information, et cetera.
So that if I print one of these
FAFlights, it looks nicer.
So that's our FAFlight.
That's essentially what's being drawn
in each of those FlightList entries.
If we back up here to the FlightList,
so this is the entire list over here.
It's this View that is this whole list
including all of these things,
it has a ViewModel as well, FlightFetcher,
which is a different thing.
Now FlightFetcher is a
very simple ViewModel.
Let's take a look at it.
It essentially takes something
called a FlightSearch,
which are search parameters for
the list of flights to show.
And this FlightSearch
is a simple little struct right here
that just has the destination
airport, origin airport,
the airline, inTheAir.
If you set these things,
then the only thing it'll show in the list
is the things that match this.
Of course destination would be like KSFO.
So the list is showing thing for KSFO.
If I specified an origin or an airline,
then would only show me
flights to SFO from that origin
or that are flying with that airline.
And inTheAir is the thing that says
whether or not we're showing only flights
that are in the air or not.
So this little struct
just essentially defines
what search we're doing.
So FlightFetcher, this
ViewModel, ObservableObject
takes one of those FlightSearches
and you can change the
FlightSearch at any time.
It makes this var
available that you can set
and anytime it changes,
it goes out to FlightAware and
starts fetching the flight.
It fetches it immediately
and then it continues to fetch it
every certain amount of
time, 30 seconds configurable
what you wanna do.
And then as the information comes back,
it just pops it into
this Array of FAFlights,
those FAFlight things we looked at,
and it's just showing
you the latest result
and it's @Published.
So of course, every time
more results come in,
this thing does its
objectWillChange.send(),
and our View like this
FlightList over here
is gonna redraw itself.
You can see that I have
this little computed var
in my FlightList of flights,
which is an Array of FAFlight.
That's just my ViewModel's latest.
It's giving me the latest
flights from my ViewModel.
And that's how I'm able
to do this ForEach.
So I'm just going through the flights.
Notice I'm using their ident,
their identity of the flight,
like SWK456 would be the
identifier of some flight
and I'm using it to go through
and put those FlightList entries together.
All right, so super simple little View.
Not much to know about here.
Most of the guts of this
program is over here
in this FlightAware code right here
that's doing the fetching.
Again you can go take a
look at that if you want.
We don't really even look
at any of it in our code,
except for this FAFlight.
We're obviously looking at the flights
to see their information.
The airport ViewModel,
this all airport ViewModel,
notice it's a shared
instance of the ViewModel.
That's perfectly fine for
one ViewModel to be shared
by many Views.
They're all looking at
the same all airports.
So we can have a shared one here
and it can give you the codes
of all the airports it knows,
and then also you can
use a subscript on it
with giving it to certain airport,
to get the info that comes back.
This airport info is
down here and FlightAware
it's what comes back from FlightAware.
Just things like the name of the airport
and where it's located, things like that.
And similar one for airlines here,
just another ObservableObject.
Lets you get all the airline info.
So we only use these few Models here
when we want to find out information
about airports and
airlines, which is rare.
Like we're doing it down here.
Of course, in our FlightListEntry,
we wanna find out the origin airport.
We're using this allAirports
here to find out what that is
and the airline that we're flying on
obviously we're going to get its name here
in the name of the flight
that we have right here,
like United 2828, American Airlines 2248.
We wanna put the name of the airline.
So we can do that as well.
Okay, so now that's all there
is to know about Enroute.
You know how Enroute works.
And what we're gonna do today
is add a little filter UI
so that we can filter through this list.
And we're gonna need a Picker to do that
'cause we're gonna be picking
airports, picking airlines,
things like that.
Picker is great for that.
This is also a great opportunity
to review modal sheets, how
we put a modal sheet up.
So let's first of all, just put the code
in our base Enroute View here
that adds a little filter button
to the upper right corner of it.
And then use that filter button
will bring up this modal sheet.
So this is all review.
We already know how to do this.
I'm gonna add that filter
Button as a navigationBarItem.
I'm gonna put it on the
trailing side actually.
I'm just gonna call it filter.
That's gonna be the name of my Button.
It's just gonna be a little
computed var down here
that's some View.
We'll have it return a Button
that says filter on it.
And inside here, when
the Button is clicked,
we're just going to set some boolean var
like showFilter = true.
Just want to have to have a
little bit of State for that.
Private var showFilter starts out false.
And when we set this Bool to true,
we're gonna have a modal sheet come up.
So we can say .sheet, right?
And we know that .sheet
takes this isPresented,
which is a Binding,
remember to this showFilter
so that whenever this Bool is set by us,
this will appear,
but also anytime the sheet is dismissed,
this will be set by the
sheet back to false.
Let's just put a Text
"Filter" on there for now.
the modal sheet
we're just gonna have it say
the word "Filter" on there
just to make sure our UI is working.
So let's take a look at this.
This hopefully should
add this filter button.
There it is up in the corner.
We've clicked filter. (cheers)
You got filter.
So this is where we're going
to put our UI with some Pickers
to pick the destination airport,
pick the origin airport,
pick the airline.
We're gonna put that all in here.
And then when we dismiss that,
it's going to update this whole thing
to show us just those airlines
or origins, et cetera.
How are we gonna do that?
We obviously need some UI to
go here, not Text("Filter").
We want it to be something
like FilterFlights,
I'm gonna call it,
and it's going to have to take
our little FlightSearch here.
Okay, remember that this FlightSearch
that we have as State in our Enroute View,
it's the thing that's saying
what we're searching with.
'Cause we pass it to the FlightList
and say, hey, Mr. FlightList down here,
please only show me the things with this.
So I'm gonna filter the flights
by essentially editing this
struct, changing this struct.
And as soon as I change this struct,
which is this var right here,
it's gonna redraw
and that's gonna cause the FlightList
to be passed a new FlightSearch
and it's all going to update itself.
So we definitely need to do that.
And then of course, since
this is a presented thing
I'm also going to have
isPresented passed to it as well
by showFilter,
and that will allow this
View, this modal sheet View
to be able to dismiss itself.
Because again, not a big fan
of just having swipe down to dismiss,
be the only way to dismiss things.
It's nice if you have done buttons
and you're gonna see we're
gonna need a Done and a Cancel
for this one.
So let's go create this View, right?
I just made this up.
So we have to go actually
implement this FilterFlights,
That is a View.
So I'm gonna make a new SwiftUI View,
and I'm gonna call it FilterFlights,
make sure that it's in the right place.
Yes, I noticed some of you
are still having a problem
where you're putting
things up here in the blue.
We want things in the yellow there.
Here's FilterFlight.
It says, "Hello, World".
Now we know that our FilterFlights
has a couple of arguments
here, these two Bindings,
so let's put those two
Bindings in right off the bat.
And we know how to pass
Bindings, @Binding to var.
We called one FlightSearch
and that is a Binding to
a FlightSearch struct.
And then we have the
Binding var isPresented,
which is a Binding to a Bool.
So now this FilterFlight
is hooked up to that.
By the way, I'm gonna have
my previews commented out
for now I really don't wanna deal
with having to try and to
pass Bindings in my previews
as we saw can be done.
We can pass constants in here, et cetera.
But we're just gonna ignore that for now
and make this demo go a little quicker
'cause we're really trying
to talk about Pickers here.
We have this FlightSearch passed into us.
Let's make sure that it's actually working
by having our Text here,
say "Filter flights
to our \(flightSearch.destination)".
So if we're properly
Binding this FlightSearch
back to our FlightSearch State
that's in our Enroute's View right here,
then when this print out,
it should print KSFO in this case.
Yes, sorry, FilterFlights flightSearch.
Put the name up here,
because that's the name of
this Binding var right here.
So let's run this.
See if our filter is now
saying FilterFlight to KSFO.
Hopefully it is ready, filter.
(cheers)
FilterFlight to KSFO.
So we are Binding our FlightSearch
to the one that's back in our routes.
And now we're really ready to proceed here
to use some Pickers and
stuff to build that.
When I go in here and
I start changing things
like let's say I change the destination.
If I change the destination SFO over here
to be somewhere else, Las Vegas or LAX
or Newark or somewhere,
it's going to cause this whole
thing over here to refetch.
My FlightAware fetch is gonna
have to get a whole new thing.
So I'm not sure that as
I'm choosing it over here
in my Picker or whatever I want behind
this thing constantly updating.
And that's what would happen
if we made FilterFlight,
this sheet, be like our PaletteEditor.
If we remember our
PaletteEditor in Emoji Art,
as we changed the name or added emoji,
it was actually changing the
palette back in our document.
That was live editing.
Our modal sheet was editing
the thing it was editing live.
So it was changing live.
I don't think we want that here.
Here I think we want a "Done" "Cancel".
We talked about this in the hints
of the homework assignment as well.
Sometimes you want live editing,
sometimes you want "Done" "Cancel".
And here I think you want "Done" "Cancel"
because you might play around
with what destination you want
then once you decide, okay,
"Done", now we'll do the fetch.
We don't wanna be wasting
money and internet resources
by fetching things that we're just picking
on the way to deciding what we want.
So we're gonna do a "Done" "Cancel" thing.
So let's put a "Done" "Cancel" on here.
Okay, I wanna put a "Done" and a "Cancel".
Now we did the "Done" Button in Emoji Art
by creating our own little title here.
I'm gonna actually
draft off NavigationView
because we know that in NavigationView,
it puts a title up here
and it has room for two
Buttons, like "Done" "Cancel".
So I'm gonna put this whole thing
in a NavigationView so I can get those.
You're gonna see that
I need this whole thing
to be in a NavigationView anyway.
So just a little bit
foresight going on here,
but it's not a bad way
to get Buttons in a title
is to put things in a NavigationView,
even if you don't plan to navigate,
which I don't really plan to navigate,
but you're gonna see I'm gonna
end up needing to navigate
in this modal sheet.
So let's put that "Done"
"Cancel" in there.
Really easy to do.
I'm just gonna say .navigationBarItems.
I'm gonna put on the leading
side, a "Cancel" button
and on the trailing side, a "Done" button.
Let's go ahead and put
a title on here too.
navigationBarTitle and call
this the "Filter Flights" sheet.
That'd be good.
So we need "Cancel" and "Done".
Those are just vars.
So cancel is some View.
It's a Button that says "Cancel".
And when this is pressed
is going to set our isPresented to false.
This is again, we pass our
isPresented in as a Binding
so that we can dismiss ourselves
and certainly cancel wants to do that,
and done, it's very similar, right?
Here's done.
It says "Done".
And it's gonna also cancel but right here,
it's going to have to
make the actual changes
because "Done" means I'm done, go do it,
do the fetch and all that stuff.
"Cancel" means just cancel
me and don't do the changes.
So we'll have to talk about
how we're gonna do this
in a moment.
All right, so we have
this navigation bar stuff,
but we need to put it in a
NavigationView of course.
I think this will work.
There we go.
Let's run.
There we go, filter.
(cheers)
There it is.
There's our FilterFlights to SFO.
We're in and NavigationView right here,
we've got Cancel and Done.
We can cancel that's good.
We can also go back here and hit Done.
That's working as well.
But of course we're not
doing any actual editing
of our FlightSearch struct,
which is what this is all about.
We wanna edit this struct.
How are we gonna do this
"Done" "Cancel" business?
One simple way to do "Done" "Cancel"
is to have your own private State here
which is a draft of what
you're trying to build.
So we're trying to edit a FlightSearch
and we're just gonna create a draft of it.
We'll have our entire UI edit this draft
and then at the end,
when it's time to make the actual changes,
we'll say self.flightSearch = self.draft.
In other words, we'll
copy into the Binding,
the bound value, this our drafts value.
And FlightSearch is a struct.
You see right here, it's a struct.
So when we say equals it copies, right?
Structs get copied.
So it will be copying what's in this draft
back into this flightSearch here.
So that works on the way out.
What about on the way in?
'Cause we essentially
wanna say something like
set this to the flightSearch, okay?
I want my draft to start out
with whatever this value
of this Binding is.
But of course we know we can't do this
because we're in the
initialization phase right here,
and this is not initialized yet
it's being passed into us
so we can't do this right?
Can't use within property initializer.
So how do we set something like this?
We need an init.
If we're gonna have an init,
it needs to have the same arguments
that this thing has over here.
So we need the flightSearch
and isPresented
as arguments to our init.
So that interesting
'cause these are Bindings.
So how are we gonna have
a FlightSearch argument here to our init?
It needs to be a Binding, right?
Somehow it needs to be a Binding here.
We're not passing a FlightSearch in here.
We're actually passing a Binding to one.
Well, remember back from lecture nine,
we talked about what these things are.
These creates struct, okay?
These property wrappers like @Binding
and @State, they create structs.
This creates a Binding struct.
So we can access that actual struct
using the underbar version of this.
And remember, there's also
the non-underbar version.
That's the wrapped value of this struct,
and then there's the
dollar version of this.
That's the projected value of this struct.
Well, here we're in initialization.
So we really can't use the
projected and wrapped values
because this needs to
be initialized, right?
Because we're in init, these
have not been initialized.
We are initializing these.
That's what we're supposed
to be doing in init.
That's what init's all about.
So we want to actually set this struct.
We're gonna say _flightSearch
equals some struct.
So we need a Binding struct right here.
So we're gonna force a Binding
struct to be sent to us.
And Binding this struct is a
generic, it has a don't-care
which is the type of the
thing it's Binding to.
So we are Binding to a FlightSearch here.
So the argument here is
Binding to a FlightSearch
and now can I say _flightSearch,
which is the actual
struct, the Binding struct
equals that Binding struct
that's being passed to us.
So this is how you can have init
where some of the vars you
need to set are Bindings.
I just declare the type of the argument
to be a Binding to the var's type
and then use underbar to
set the actual struct here.
And we can do the same
thing with isPresented here.
That is a Binding to a Bool.
And so we can also here say _isPresented
equals isPresented, the
argument to this function.
So how about the draft?
How do we set the draft?
We kinda like to say self.draft
equals this flightSearch.
And that's what we're trying to do,
but we can't really do this.
These two vars are not of the same type.
This right here is a
Binding to a FlightSearch,
that's its type,
and this is the wrapped value of this,
which is a FlightSearch.
So that's why it's saying
you can't assign a
Binding to a FlightSearch
to a FlightSearch, right?
This FlightSearch is not
the same type as this draft
'cause we're doing the
draft wrapped value here.
So we're gonna have to
initialize this State
like we learned to do last time
by sending its _draft equal to a State
with some initial wrapped value.
And what is the wrap value?
Well, interestingly,
it's not the flightSearch
because the flightSearch
here is this Binding.
It's the flightSearch's
wrapped value, right?
A Bindings wrap value is
the thing it's bound to.
So flightSearch's wrap value
is the FlightSearch that it's bound to.
So here I'm creating this draft state
initializing its struct
by creating a struct, a State struct,
whose wrapped value is the same as
the flightSearch's wrapped value.
This is why I spent so
much time in lecture 9
going over what these
property wrappers really are
so that you'd hopefully
understand code like this.
We've copied this draft
in from our FlightSearch.
Let's double check that we
have by going to our body.
And instead of showing our
flightSearch's destination,
let's show our draft's destination.
So if this copy has happened correctly,
then this draft will be showing properly.
So let's take a look.
All right, filtering, here it is.
Oh, filter flights to SFO.
That's our draft destination, excellent.
So it obviously copied
our flightSearch in.
So now we're actually almost done.
All we need to do going forward here
is change our UI in here
from just being a TextField,
to being a bunch of Pickers and things
that are editing this draft.
We're automatically gonna
copy that draft back out
if we click the "Done" button,
we'll do nothing if we hit "Cancel",
so the draft will be ignored,
but this makes it really easy
for us to implement in here.
So let's do our first Picker.
Let's do draft destination.
Let's add a Picker that lets
us choose a new destination
for our search.
Picker has a few constructors,
but we're almost always going to use
this second one right here
that takes a title of
either a LocalizedStringKey
or a title.
And you can use a Text.
That's what this one up here is
that lets you specify
a label for the title,
but it's very common in Picker
to have the title just be a String.
So let's go ahead and do this one.
The title of this, I'm
gonna call "Destination"
because I'm setting the
destination airport here.
The second argument here to Picker,
very important one.
This is a Binding to the thing
you want to change with the Picker.
So for us, we wanna change
our draft's destination.
That is what this Picker is trying to do,
choose a new destination
in our draft FlightSearch right here.
And then this last argument content
is the list of Views that
are gonna be in our Picker.
One thing Picker,
interesting to understand is
you are going to provide a list of Views.
That's why in Picker we're
almost always gonna do a ForEach.
That's the easiest way we know
to provide a list of Views.
This is a list of Views.
And this Picker is essentially
picking between those Views.
And we're gonna show you how it determines
when you pick one of those Views,
what to update your selection to.
So let's get the Views first.
Again, we're gonna do a ForEach.
Now I want to be able to pick an airport.
So I need all my airport codes.
So I'm gonna using allAirports.codes.
We'll see how to do that in a second,
and this is String so I'm gonna do .self.
This is a ForEach after all
and this ForEach it's
gonna say airport in.
Here let's just print out the airport.
These are the airport codes
like KSFO or whatever.
Now how do we get this allAirports thing?
We're gonna have a ViewModel,
the same ViewModel I use down here
in the FlightListEntry to get allAirports,
this little ObservedObject,
I'm gonna copy and paste
that one over to here,
and it's gonna be part of
the ViewModel for this View.
And this var provides all the codes
for all the airports that
this ViewModel has ever seen.
Now we're not quite done here.
This is making a bunch of Views
that are gonna be in the Picker.
So the Picker is gonna be able to have
a View for every code,
but how do we match this
selection up to this View?
We do that with dot tag
and I'm gonna tag this with the airport.
So whenever this View is
clicked on in this Picker,
and we'll see what a Picker
looks like in just a second,
whenever you pick this View, essentially,
it's going to take this tag
and put it in this variable.
And again it's got a
Binding to this variable,
so it just puts it right in there.
And what's really important about Pickers
is that this tag has to
have exactly the same type
as this Binding is bound to.
Otherwise this makes no sense.
So luckily this is true here,
this airport code is just a String.
Our destination over here
is an airport code String right there.
So this String is exactly the same type
as these airport codes, which are Strings.
Let's see what this looks like.
All right, here's our flights.
(cheers)
Oh, well here's a Picker folks.
Okay, it's a wheel.
You can go around, pick
a different thing LAX,
Las Vegas, right there, Newark.
But this is incredibly ugly.
All right? (chuckles)
It's first of all floating off
in the middle of space right here
and also its destination
doesn't quite fit there, this title.
So this is really, really bad.
And SwiftUI is doing the best it can
given the environment that
you're asking it to show in
and really it's our fault
because when we're doing
something like this,
we know that the environment
we really wanna be in is a Form
just like we did with our palette editor.
We put all the fields in a Form,
we wanna do the exact same thing here.
There's something very interesting
that's going to happen to
this when we put it in a Form.
Now this should be working, okay?
We are picking this here's Las Vegas.
I'm gonna hit Done.
It should re-fetch Las Vegas and it does.
So it is actually editing
it, so nothing wrong there.
It's just the look of
it is so bad right now.
So let's put that thing in a Form.
Watch what happens when
I put this into a Form.
All I'm doing no change really,
except for just putting
this Picker inside a Form
and hitting run.
Filter, oh, completely different look.
This Picker has completely adapted itself
to the fact that it's in a Form.
And this is a fundamental
aspect of SwiftUI.
It adapts its controls to the
environment that they're in.
And I mentioned this earlier with Button.
Why do we use a Button
instead of just Text
with onTapGesture on it?
Well, we use a Button
because the Button knows
whether it's on an Apple watch
or an Apple TV, but it's
not just cross platform,
it's within a platform.
If you have a Picker and it's in a Form,
it knows it can use this
really cool form right here,
where it has the title
and the value side by side
and it doesn't need the wheel,
because when I click on
this, watch what happens.
It navigates to a list of
all of the airport codes.
And I can pick it here, pick
San Diego over here, Las Vegas,
and this is a much better way of doing it.
Now, one thing about the adapting
to your environment, however though,
you have to be a little bit careful,
is that it adapts because
it knows it's in a Form.
It doesn't necessarily adapt
because it knows it's in a NavigationView,
even though this adaptation
requires a NavigationView, right?
When I click on here, I'm
navigating to this View
that it's wonderfully building for us.
And if I took this NavigationView away,
let's comment this out and
go back and see what happens.
It's still gonna adapt
to a form right here,
but I can't click on it.
I'm touching it right
here nothing is happening
because I'm not in a NavigationView
so it can't navigate.
Now, if I really didn't want
this in the NavigationView,
I do because I want "Done" and "Cancel",
but if I didn't want it,
I could make this go
back to being a wheel.
So Pickers here have a .pickerStyle,
and you can say, I want this
to be the WheelPickerStyle(),
no matter what the context is.
So now we go back, we're still in a Form,
but we've got this wheel
format or look again.
Still looks bad.
So let's go back to
using our NavigationView,
it definitely looked a lot better.
Let's try and understand what
this Picker has built here.
This obviously is a List
that is built, okay?
List, capital L, List of all
the Views in this ForEach.
You see this ForEach
that's making these Texts,
these are just the Texts in here.
That's all there are in here.
But one thing that's interesting to notice
is that back here, this is
also one of these Texts.
It's the text whose tag matches
the thing we're bound to.
So this View actually appears twice.
It appears here and it appears over here
down where?
Way down here with a
little check mark by it.
So that View is used in both places.
You have to understand that
when you're making a Picker,
that this View that you're picking,
you're actually picking the View.
Now, the View is what's appearing here
and it's being reported
back to you via this tag.
So we've got this working for destination
and let's make sure it's still working.
Change the look of this thing here,
but let's change our
destination to Boston.
(cheers)
Flight into Boston.
All right, so we've got that working.
Now, let's add another thing
besides choosing our destination in here,
let's choose our origin airport.
So we only show flights to SFO from LAX
or something like that.
We can do that with almost
exactly the same code here.
This is not our destination.
We're doing our origin right here,
and the selection is our origin.
Otherwise it's the same,
we're looking for all
airport codes, et cetera.
So let's see if this works.
Right here, they're coming to SFO,
we filter destination,
looks like it's working great, origin.
Okay, this is blank.
Maybe that's okay because
we don't have an origin yet.
We didn't choose an origin, it's nil.
All right, we'll go in here.
Here's our choices.
Let's choose Denver.
Oh no, it didn't work.
Why can't I pick an origin airport here?
It keeps being blank.
And is it actually searching?
No, it's still showing
me all airports here.
So why is this not working?
Well, this is a common thing.
When people use Pickers for the first time
they miss this tiny subtlety
here that's going on.
I told you that this var
has to be exactly the
same type as this tag.
This var, what's its type?
The drafts origin?
This Optional String.
Oh, Optional String.
And what's this type?
Oh, that's a String,
'cause this is a ForEach of Strings.
So this is a String, this
is an Optional String,
that's not gonna work.
They're not exactly the same type.
They need to be exactly the same type.
Now a tricky fix to this actually
is to make this argument to
our ForEach closure here,
be a Optional String.
So remember this is equals void.
But we're just using Swift inference
to not have to specify these things.
But here I am gonna specify
it and make it this type.
Now why doesn't this break the ForEach?
Because the ForEach just
wants you to give it a closure
that you can pass its
thing into that will work.
And of course you can pass
a String into a closure
that takes an Optional String.
Now this has changed airport inside here
to be an Optional String.
So now this tag is an Optional String
and this is an Optional String.
So that fixed our problem
where these were not the same type.
However, kind of broke
us a little bit here
because it's saying airport
here is now an Optional,
and so we have to unwrap
that Optional in here.
So I'll unwrap it with the
defaulter that says something
when airport is nil.
And what do we want
when the airport is nil?
We want it to say "Any"
airport or "Unchosen" airport
or something like that.
So let's see if this works.
To our filter here.
Well, origin, that's still blank.
So that's still not right, it's not right.
Going here.
Okay, this is good.
I don't see the choice of "Any" airport,
but let's try something like Denver.
Oh, at least this is working now.
When I pick something in
here, Dallas, it's working,
but that's because we've made these match.
But there's no way for me to
go back to origin "Any", right?
I can't set this back to nil.
Why can't I set this origin
anywhere here to nil?
The answer is that this list of Views
is made from this closure right here,
and right now it's a ForEach
of all the airport codes.
There's no airport code which is nil.
So since there's no nil in there,
there's no nil in this list.
So if we wanna be able to choose "Any",
we have to put nil into this list.
But this does not have
to be just the ForEach.
We could go here and say Text("Any"),
it's got the tag of nil, and
we've added another View.
So there's all these ForEach's,
but then there's this other one
right at the beginning here.
Now this doesn't quite work
because Swift can't infer that
this nil means String's nil.
That's what this generic
parameter "V" cannot be inferred.
If we go look at tag, we can look at tag,
let's see if we take tag
zero, can probably look.
Yeah, here's documentation for tag
and you can see the
tag takes a don't-care.
Tag doesn't care what type
you give it as the tag,
all it requires
is that this be something
that's Hashable right here.
But if you just say nil,
then it just doesn't
have enough information
to know what nil are we talking about,
'cause it could be anything.
So how do we specify that
we want nil of String?
There's a cool way to do
that, String Optional.
Remember that's an enum and
we can use the case .none.
Right, we could also use
.some of some String,
but we're gonna use .none.
So this is a way to say
nil for Optional Strings.
It's a tricky little way to do that.
So now we've added an extra thing.
And when we go back to our filter,
we should see another row
that says "Any" in it.
And it's even got the View here.
(cheers)
There's that any row up there.
So we can go over here to
Boise and then come back,
go back to any and we're at any.
And if we say done, (cheers)
it's still showing any.
And if we go over here and go origin,
let's do, I dunno, Denver, maybe.
So to see if we can find a flight.
(cheers)
There, it is a flight from Denver today
arrives at 1:00 in about
half an hour, all right?
So that's what's going on
here when you have a nil
or an Optional value and you
wanna be able to choose nil,
you need to add a little
extra View just for that
because the Picker is picking Views.
It picks one of these Views.
So you gotta have a View
for every option you want.
Let's now go do the same
thing with airlines.
We can filter by airlines.
So I'm gonna put "Airline" here.
To make my life easier
I'm gonna replace airport with airline
because it's almost
exactly the same thing,
paste, paste, paste,
and I will need to add
another ViewModel up here,
which is all my airlines,
airlines,
airlines,
Okay, so now I have a ViewModel
for all my airlines and my airports,
so I can do that with my airlines.
And this airline
is of course airline in
our draft FlightSearch.
Yeah, here we go filter, origin,
yeah, we could pick origin, that's good.
Airline, any airline or maybe United.
So let's see United flights into SFO.
Excellent, right there.
And we can go back to any and hit Done,
Get the full list of flights.
Now, one other thing that
I'd like to make better
is these flight codes I know them well,
as you can tell, but a
lot of people don't know
what are these things?
What's K-O-R-D?
What the heck is that?
We want these to be nicer
names than KSFO and KORD.
We want to be names of actual airports,
and we can do that too.
These ViewModels up here,
allAirports and allAirlines
can provide us a lot better information.
So here, instead of just
putting the airport code,
I'm still gonna have the
tag be the airport code,
but instead of showing the airport code,
I'm going to use my self.allAirports,
this airport right here,
and that might be nil,
they might look it up and
not be able to find it,
so I'm going to Optional chain here
and use what I call its friendlyName.
And if that's not set,
then we'll just use the airport code.
So this is just getting a friendly name
from this ViewModel up here,
this allAirports ViewModel,
a friendly name of this airport.
And we can do the same thing
for this airport down here,
and we can do a similar
thing for the airlines,
the allAirlines.
So it has a friendly name feature.
We'll go to our filter.
Okay, destination, not KSFO anymore.
San Francisco, California.
(cheers)
These are all the friendly
names of these airports.
How about airlines?
Yeah, we got the friendly names.
So let's add one more thing,
not a Picker, but a Toggle.
Toggle takes a Binding to
something that it's gonna Toggle.
In this case I wanted to
Toggle my draft's inTheAir
and it also takes a label
and that'll just be for us a simple Text
that says "Enroute Only".
So this Toggle is going to Toggle
whether we're showing
flights that are in the air
or flights that are on
the ground somewhere.
Take a look at that.
We got Enroute Only.
So I'm gonna turn this
off and go back to Done.
(cheers)
We got a lot more flights
here, not departed yet,
flights that are
scheduled to arrive later,
but haven't departed.
(mouse clicking)
All right, so that is all I really wanted
to cover today on Picker.
And hopefully that sets you up
to use Pickers in your final project.
Most of you I'm guessing
are going to have somewhere in
your UI in your final project
where you're gonna wanna do a Picker
and now you know how to do it.
- [Narrator] For more, please
visit at us stanford.edu.
