Lecture 11: Picker

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
(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.
Info
Channel: Stanford
Views: 8,506
Rating: 4.9710145 out of 5
Keywords: SwiftUI, Xcode, iOS, iPhone, iPad, Swift, Stanford, CS193p, coding, iOS programming, Enroute, FlightAware, REST API, JSON, airport, airline, Picker, Toggle, Form, NavigationView, Binding, Hashable, Codable, Comparable, CustomStringConvertible, navigationBarTitle, navigationBarItems
Id: fCfC6m7XUew
Channel Id: undefined
Length: 49min 27sec (2967 seconds)
Published: Mon Jun 22 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.