This week, we will make an app that allows users to track where their car is parked and where they are relative to the car. We will be using MapKit, SwiftUI, Location, and plists to save car data. Here is a sneak peak of the final app, but let's dive in!
-
Download the starter code at: https://github.com/67443-Mobile-Apps/FindMyCar_starter
-
The first thing you need to do when working with locations is getting permission from the user to use their location. To do this, go into the
Info.plist
file found in the file explorer and add a new property list item calledNSLocationWhenInUseUsageDescription
and add in the text "This app would like to use your location." (This can be done by clicking the+
icon shown by hovering over "Information Property List"). This is the message that will be displayed when your app requests permission from the phone to use location services. -
Create a model file called
Location.swift
. Afterimport Foundation
add in the directiveimport CoreLocation
. Then add in the following code:class Location: NSObject { var latitude: CLLocationDegrees var longitude: CLLocationDegrees var locationManager = CLLocationManager() override init() { self.latitude = 0.00 self.longitude = 0.00 super.init() } func getCurrentLocation() { locationManager.requestWhenInUseAuthorization() if CLLocationManager.locationServicesEnabled() { locationManager.distanceFilter = kCLDistanceFilterNone locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.startUpdatingLocation() } if let currLocation = locationManager.location { self.latitude = currLocation.coordinate.latitude self.longitude = currLocation.coordinate.longitude } } }
Right now this class mostly tracks (and will later store) a location's latitude and longitude, but it also has the
getCurrentLocation()
method. One thing to note ingetCurrentLocation()
is that the first thing the app will do is verify that it has permission to use the phone's location services. The user will only have to give permission once (they can later change it in their phone's settings) but the app will always check to see that it has permission before calculating the current location. In some cases the initial granting of approval can take a second or two for the device to process, so the initial location will be at (0.00, 0.00) because the authorization hasn't been processed yet even though the user literally just gave permission.
-
Go the
ContentView.swift
and add two button:Here's My Car
andWhere's My Car?
. At this point, you app should look like the left screen at the beginning of the lab. (You may need to addSpacer()
s to get the spacing right.) -
Create a new
SwiftUI View
calledMapView.swift
. -
Add in the following functions:
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
let coordinate = CLLocationCoordinate2D(latitude: 40.4454261, longitude: -79.9437277
)
let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView(frame: .zero)
return mapView
}
- Now, replace the
Where's My Car?
button in theContentView
with aNavigationLink
to theMapView
. - Test out the app! The
Here's My Car
button is not yet functional, but when you press on the other button, you should see your car near the Tepper Quad. Feel free to experiment with theMKCoordinateSpan
to find a radius that you like best. Also note you can hold down theoption
key in the simulator and drag around to perform a pinch-to-zoom effect.
-
Create a new controller file called
ViewController.swift
. In this controller, create two instances ofLocation
--one for the user's current location and one for the location of the car. -
Right now, let's just look at the user's current location. Create an instance of the
ViewController
in theContentView.swift
and a variable to reference it in theMapViewController
. Change the constructor in theMapView_Previews
accordingly like we did in the last lab. Update yourNavigationLink
as well. -
Now in the
MapViewController
, let's make sure that the map is always centered around the user. To do so, add a line to get the user's current location at the top of theupdateUIView
method and change the hardcoded values for latitude and longitude to be the user's current latitude and longitude. -
Let's also create a pin for our location. Luckily, doing this in iOS is super easy; just add the following code to the end within the
makeUIView
function:-
let droppedPin = MKPointAnnotation() droppedPin.coordinate = CLLocationCoordinate2D( latitude: // fill in with your current location latitude , longitude: // fill in with your current location longitude ) droppedPin.title = "You are Here" droppedPin.subtitle = "Look it's you!"
Add
mapView.addAnnotation(droppedPin)
right before youreturn mapView
.Also, make sure you keep the comma between latitude and longitude above :)
-
Rerun the project to see the pin and the title beneath. Note the subtitle after you click on the pin. You should see something like the second screenshot at the top of the lab.
-
-
Test this out in the simulator!
Note: Your simulator doesn't have GPS in it, but you can give it coordinates that it might have gotten from a GPS unit. To do this, go to your simulator and choose Debug > Location > Custom Location
.
-
Let's go back to the
ContentView.swift
and fill in the action for ourHere's My Car
Button. We are going to get the current location of the car like we did for the user but also display an alert that the car's location has been saved. -
To do this, let's first generate the message the user is going to see. Add the following functions to your
ViewController.swift
so you can use them in theContentView
.func generateTitle() -> String { let message = "Your car is currently at:\n(\(self.carLocation.latitude), \(self.carLocation.longitude))" return message } func generateMessage() -> String { let message = "\nWhen you want to map to this location, simply press the \"Where is my car?\" button." return message }
-
Run your project and verify it is working as expected by tweaking the locations in the simulator to map to different locations. FYI, the British Prime Minister's home is at (51.5034070, -0.1275920) and the White House is at (38.8976090, -77.0367350) if you want to try funky, non-Pittsburgh places to test. Here is an example of the alert:
-
We want to test that the location we saved is reflected upon pressing the "Where's my car?" button. Make sure in the
MapView
that the pin we dropped earlier on our current location is eliminated and a new pin is now dropped on the current car's location. Feel free to also change the Title/Subtitle of the pin accordingly. In addition, change the map to center to on the car's location and not the initial location as we did earlier. -
We know where our car is and there is a pin dropped on the map to make it clear, but it'd be nice if we knew that location relative to our current position. This is easy, simply add the following line to your
updateUIView
method in yourMapView
:uiView.showsUserLocation = true
In the simulator, assuming the car is still parked in Morewood (40.4454261, -79.9437277), change the location to Wean Hall (40.4426092, -79.9454014). When you press on the show me the car button, the red pin drops on the car and your location has a blue glowing button. In a real mobile device with GPS, this blue dot will readjust as you move.
Now we will take a look at implementing a way to save our car's state no matter what may happen to our device: the app dies or the phone shuts down to name a few. We will do this for now by using a plist file, which is essentially writing some information to a file. In future weeks we will talk about using CoreData, which is an iOS framework for accessing internal memory similar to a database.
You can see the Contacts example from lecture as a means of implementing plists in an app to save state.
For now, let's step through the steps to implement saving and loading our coordinates in the
Location
object.-
Let's begin by adding this string extension to the top of the
Location.swift
file (outside of theLocation
class):extension String { // recreating a function that String class no longer supports in Swift 2.3 // but still exists in the NSString class. (This trick is useful in other // contexts as well when moving between NS classes and Swift counterparts.) /** Returns a new string made by appending to the receiver a given string. In this case, a new string made by appending 'aPath' to the receiver, preceded if necessary by a path separator. - parameter aPath: The path component to append to the receiver. (String) - returns: A new string made by appending 'aPath' to the receiver, preceded if necessary by a path separator. (String) */ func stringByAppendingPathComponent(aPath: String) -> String { let nsSt = self as NSString return nsSt.appendingPathComponent(aPath) } }
This extension allows us to append a filepath component to a given string, allowing us to create the filepath we will need to the plist file.
-
Now, we must define the Swift functions inside the
Location
class to retrieve the appropriate directory for our plist file and the appropriate final filepath including our plist file in the device's memory:func documentsDirectory() -> String { let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) return paths[0] } func dataFilePath() -> String { return documentsDirectory().stringByAppendingPathComponent(aPath: "Coordinates.plist") }
We should then print the dataFilePath out of the
init
function, so let's add that now as well. -
Now we are ready to write a function to save the location of the car which we will later call in several places around our app's files. This function will take our current latitude and longitude and save it to the plist. Add the
saveLocation
function in theLocation
class:func saveLocation() { let data = NSMutableData() let archiver = NSKeyedArchiver(forWritingWith: data) archiver.encode(self.latitude, forKey: "latitude") archiver.encode(self.longitude, forKey: "longitude") archiver.finishEncoding() data.write(toFile: dataFilePath(), atomically: true) }
-
Similarily, below the
saveLocation
function, add theloadLocation
function which we will call to retrieve the latitude and logitude from the plist and save them to our location object:func loadLocation() { let path = dataFilePath() if FileManager.default.fileExists(atPath: path) { if let data = NSData(contentsOfFile: path) { let unarchiver = NSKeyedUnarchiver(forReadingWith: data as Data) self.latitude = unarchiver.decodeDouble(forKey: "latitude") self.longitude = unarchiver.decodeDouble(forKey: "longitude") unarchiver.finishDecoding() } else { print("\nFILE NOT FOUND AT: \(path)") } } }
-
We should also write a function to clear out our latitude and longitude before saving. Add the
clearCarLocation
file to theLocation
class:func clearCarLocation () { self.latitude = 0.00 self.longitude = 0.00 }
-
The last thing we need to do is update our
getCurrentLocation
function in theLocation
class to clear the car's location and save the updated location to the plist. CallclearCarLocation
to the beginning of this function, and callsaveLocation
right at the end.
The only things that need to be done are to switch the call from
getCurrentLocation
toloadLocation
so we load from the plist inupdateUIView
and to addloadLocation
to the top ofmakeUIView
.All that needs to be done here is to clear, get, and save the location of the car to our plist in the function attached to the button in the UI. Call the function to cleat the car location before getting and the function to save to the plist after getting the car's location.
For
AppDelegate.swift
we must invoke our actions to save and load the location of the car on various triggers.The car's location should be (loaded first, and then) saved in:
didDiscardSceneSessions
While the car's location should be just loaded in:
didFinishLaunchingWithOptions
configurationForConnecting
Now the plist should be fully integrated into the app! Try adding location, killing the app, and checking to see it is there. If you are having issues with the plist, be sure it is being saved properly to the path printed when the
Location
object is initialized. -