Mobile Development Lab 3 Objectives Illustrate closures through examples Have fun with maps, location and geolocation Have fun with animations Closures implemented in Swift Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages. Closures are used in Swift/Cocoa on various frameworks, it is useful to understand their use for various tasks. In this lab work, we will introduce closures for both geolocation and animations purposes. There are three kinds of closures: global functions have a name and cannot capture any values nested functions have a name and can capture values from their enclosing functions closure expressions don t have a name and can capture values from their context We show below multiple valid syntaxes for closures: implicits and shorthands allow to simplify syntax but with a loose of readability. For clarity, we declare the closures, but usually they are used inline, i.e.: directly as an argument function. var oneparameterandreturnvalue = { (x: Int) -> Int in return x % 10 var oneparameterandreturnvalueimplicit = { (x) -> Int in return x % 10 var oneparameterandreturnvalueshortcut = { return $0 % 10 var multipleparametersandreturnvalueimplicit = { (first, second) -> String in return first + " " + second var multipleparametersandreturnvalueshorthand = { return $0 + " " + $1 Closures are also useful to work with arrays. Similarly to lambdas in Python language, closures allow to map, filter, reduce arrays items. They are also useful to apply any function taking a function as argument (ie: sorting an array). - Consider that we need to sort an array of numbers. let numbers = [10, 3, 2, 5, 1, 4] - We could declare a closure for that and call it within sorted array s function: var sortfunc: (Int, Int) -> Bool = { (num1, num2) in return num1 < num2 numbers.sorted(by: sortfunc) Note: As sorted function is called on an array of Int, Xcode s completion asks for the by argument to have type (Int, Int) -> Bool - The closure could also be defined inline: numbers.sorted(by: { (num1, num2) in
return num1 < num2 ) - And syntax could be minimalized with implicits and shorthands: numbers.sorted{ $0 < $1 In the previous lab work, remember that we had to modify one by one the items of an array (we had to escape-decoding an array obtained from a JSON dictionary). And we used enumeration: for (i, name) in namesarray.enumerated() { // replace escape characters namesarray[i] = name.removingpercentencoding! Closures could have been useful with array s map function: namesarray.map({ (name) -> String in name.removingpercentencoding! ) With trailing closure, we don t need parenthesis: namesarray.map{ (name) -> String in name.removingpercentencoding! And with shorthand, it s much more shortened: namesarray.map{$0.removingpercentencoding! A geocoding and geolocation project We will now make a mobile application that go further in using closures on Cocoa frameworks. Maps, location and geolocation are features that can (more or less) only be implemented in mobile apps. We will first use Apple s Geocoder service to obtain latitude/longitude of a given address. Then, using delegation, we will track user s location and display on a map view. Still with closures, we will add UIView s animations to obtain a nice looking app. - Create a new project with template Single View Application ; name it MyGeoTracker - As we will use maps, we need to link our project with MapKit framework: several option for this. One simple, in Project s setting window, select tab Capabilities, and swich on Maps. - In top of ViewController.swift, add: import MapKit - Still in ViewController.swift, declare outlets for one UITextField and a MKMapView, add the corresponding UI object in Storyboard and connect them. @IBOutlet var addresstextfield: UITextField! @IBOutlet var mymapview: MKMapView! - We ll need to trigger a function when Enter key is pressed from adressetextfield: add the needed delegate. Handling geocoding - Declare a variable property of class CLGeocoder and read its documentation s description. let geocoder = CLGeocoder() Let s first implement a simple forward-geocoding feature: given a address, move the map to the corresponding location. - Add a function forwardgeocoding(withaddress:) taking a String as argument:
func fowardgeocodding(withaddress address:string) { - Call this function from textfieldshouldreturn(). Let s now implement our function forwardgeocoding(withaddress:) and use the geocoder object. The needed function uses a closure as completion handler. In closure, note that we only need its first variable, so we just ignore the second (with _ ). The first closure s variable placemarks is an optional array of CLPlacemark. The array may have zero item if nothing is found, or may have multiple items in case where multiple locations respond to the same given address. For safety, we need to use it within a if-let statement, and as we then get the first item (which is optional too), we unwrap everything in an optional chaining. To change the location of UIMapView, we can either set its property centercoordinate of type CLLocationCoordinate2D ; or call its function setregion() (that uses a MKCoordinateRegion and allow to animate the transition). And, an object of MKCoordinateRegion is constructed from CLLocationCoordinate2D and region s size in meters: geocoder.geocodeaddressstring(address, completionhandler: { placemarks, _ in ) if let placemark = placemarks?.first { let coordinateregion = MKCoordinateRegionMakeWithDistance((placemark.location?.coordinate)!, 1000, 1000) self.mymapview.setregion(coordinateregion, animated: true) With closure shorthand, it s equivalent to: geocoder.geocodeaddressstring(address) { let placemarks = $0.0 // shorthand's first member if let placemark = placemarks?.first { let coordinateregion = MKCoordinateRegionMakeWithDistance((placemark.location?.coordinate)!, 1000, 1000) self.mymapview.setregion(coordinateregion, animated: true) Great! We have our first geocoding feature. Let s now add geolocation Manage Geolocation We will add a geolocation functionality to our application. The application will count the distance traveled (since his last start or reset) and will display the current position and trace the path on the map. Setting Xcode for asking user s location We first must tell Xcode to allow our app to access user s location. To protect user privacy, an ios app which accesses the user s location information, must statically declare the intent to do so. We must include the NSLocationWhenInUseUsageDescription key in the app s Info.plist file and provide a purpose string for this key. Add the following key (anywhere between existing keys): <key>nslocationwheninuseusagedescription</key> <string>this app tracks your current location, please allow!</string> The first time the app needs to access user s location, it should ask the user to allow for accessing current location with the given message. This decision can be changed at any time by the user from the Setting app. Initialization and configuration of CLLocationManager The framework CoreLocation contains a class CLLocationManager which allows to access, start and stop the different device s location resources (GPS, Heading). We need to instantiate an object of this class.
- In ViewController.swift, declare a variable (property) of type CLLocationManager and instanciate it with default initializer. let locationmanager = CLLocationManager() - Find in the documentation (or browse header files) to get informations about how CLLocationManager works. You found the protocol associated with this class? Indeed, CLLocationManager also uses delegation to operate. Functions startupdatinglocation() and stopupdatinglocation() are respectively used to start and stop the location service, while delegate s function locationmanager(didupdatelocations:) of CLLocationManagerDelegate is called every time the position has changed. - In our case, the class ViewController will be the delegate of CLLocationManager. Change the declaration in ViewController.swift accordingly: class ViewController: UIViewController, UITextFieldDelegate, CLLocationManagerDelegate - In viewdidload(), we can configure our locationmanager (look especially its properties distancefilter and desiredaccuracy), then add: locationmanager.delegate = self Since ios requires an authorization to access user s location and the user may change its decision at any time, we must first check the authorization status. If it is still undetermined (ie: first time launching), it is necessary to request the right permission. If the status is already valid, we can start the location service. - In viewdidload(), after locationmanager configuration, add the following statements: let status = CLLocationManager.authorizationStatus() if (status ==.notdetermined) { locationmanager.requestwheninuseauthorization() else if (status ==.authorizedwheninuse) { locationmanager.startupdatinglocation() else { print("not allowed to access current location") - At the first launch (when status is undetermined), the delegate s function locationmanager(didchangeauthorizationstatus:) is called. If everything is ok, it is time to start the location service. Add the following function: func locationmanager(_ manager: CLLocationManager, didchangeauthorization status: CLAuthorizationStatus) { if (status ==.authorizedwheninuse) { locationmanager.startupdatinglocation() else { print("not allowed to access current location") - To go further: https://developer.apple.com/library/content/documentation/userexperience/conceptual/locationaware nesspg/introduction/introduction.html
Looks great but nothing happens Sure, we didn t yet implement any tracking feature. Indeed, we only prepare the location manager, we will add tracking in next section. Notice that it is possible to simulate location movement on the Simulator: Menu Debug/Location/City Run... Tracking and distance travelled We will now focus on monitoring the user s location. To illustrate it, the application will count the distance traveled since the start of the service. - In ViewController.swift, add a variable totaldistance (of type Double) initialized to 0. When the position is updated, the function locationmanager(didupdatelocations:) is called. The second argument is an array that contains CLLocation items stored in chronological order (the last one is the most recent). We can compute the get the distance in meters between two CLLocation through its instance function distance(from:). - In locationmanager(didupdatelocations:) get the last item of the array locations and get its distance to the previous location. We also need to store the previous location in a class variable previouslocation. let currentlocation = locations.last! totaldistance += currentlocation.distance(from: previouslocation) previouslocation = currentlocation - Add a new UILabel to your view and display the distance traveled. Be careful, the first few calls to this method return often very far locations. You can check the estimated accuracy from properties horizontalaccuracy and verticalaccuracy, or you can just ignore the first five calls to this method. - Implement tracking in the map: the map region should follow the current location (see above how to change map s region) - Add a UISlider named zoomslider and an associated IBAction to change the map s scale. From Storyboard, set minimum value to 10 and maximum value to 2000. Change your code in order to make map s region match with slider s value. - Add a UISwitch named geofeatureswitch to pause (starts / stops) geolocation and connect it to the IBAction switchchangedvalue(). Searching for an address (geocoding) will only work when the switch is off. You might also want to write "Enter an address" in the UILabel when the switch is off and hide the UITextField when the switch is on Is it working? Great, let s now draw on the map Drawing the path If you did not already, you must first conform ViewController with the protocol MKMapViewDelegate, then add the following statement in viewdidload() mymapview.delegate = self
To draw on the map view, several solutions are available to us. We will use the MKMapView and MKMapViewDelegate drawing overlay feature. The "viewable" objects on the map must implement the protocol MKOverlay: we will add items of MKPolyline that implement this protocol. - In the function locationmanager(didupdatelocations:), initialize a variable MKPolyline from two points: previous and current locations. MKPolyline needs two arguments, an array of CLLocationCoordinate2D (got from CLLocation s property coordinate, and the number of items in the array (here, it is 2). Then, pass it to your mymapview using function addoverlay(). The visual rendering of these objects is determined in a second step by calling the delegate method mapview(rendererforoverlay:) (of delegate MKMapViewDelegate). Here is a way to render a MKPolyline object: func mapview(_ mapview: MKMapView, rendererfor overlay: MKOverlay) -> MKOverlayRenderer { let linerenderer = MKPolylineRenderer(overlay: overlay) linerenderer.strokecolor = UIColor.green linerenderer.linewidth = 5 return linerenderer So cool! We are now drawing on the map Let s add one more feature: gesture shaking. Reset tracking by shake gesture detection Before completing this lab, we will add one more functionality to our application: we want to reset tracking (and distance) by shaking the device. Reset consists in: - Set totaldistance variable to 0 - Remove the MKOverlay objects to the map. Look at the documentation: a MKMapView object can delete multiple MKOverlay passed as arguments in an array. We will then basically remove the array of overlays associated to our map, through mymapview.overlays. - To avoid unexpected behaviors, do reset only when UISwitch is off. Several solutions are available to us for detecting a shake. We will see a natural way to do it, based on the detection of "Gesture" (Alternatively, a lowest level solution would use CoreMotion framework). Our class ViewController inherits from UIViewController which extends UIResponder. UIResponder is the class responsible for event management (Touch, Motion,...). Because of this, we can directly override any of the methods of UIResponder. - Browse the documentation and search for the name of the function that will capture the event "Shake Motion. - Add this function in ViewController and implement the behavior described above. Let s now add a simple visual feedback to the user. The visual feedback could consist in displaying a red message "Reset tracking" for a second in our distancelabel. It means that we first change distancelabel to show the red message, then a second later we turn distancelabel to its previous settings...
As we seen previously in Lab 2, we could use a Timer for this purpose. We already used a timer using a selector, let s use a timer in a more modern and convenient way, using closures (here with trailing syntax): // make new string instance from existing string let previoustext = String(describing:distanceLabel.text!) distancelabel.text = "Reset tracking..." distancelabel.textcolor = UIColor.red Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in self.distancelabel.text = previoustext self.distancelabel.textcolor = UIColor.black Notice that we also could use Grand Central Dispatch framework to manage asynchronous and delayed tasks, we will use it in next lab work. It should work Let s slightly improve our UI, using animations! Improving the User Interface This section aims at enriching the visual rendering of the application for a better interaction with the user. We already added a simple visual feedback on shake gesture. We'll now add few animations to show/hide our UITextField according to the UISwitch s state. Then we will change the behavior of the map: in addition to track the position, we will rotate the map with the user s direction. Animations provide visual effects required for better interaction with the user, enabling better understanding, and thus contribute to a better overall perception of application. Obviously, the animations are only related to graphic elements of UIKit. In particular, some properties are "animatable". By using simple closures, we just define the final state in which the object should be, and CoreAnimation does the intermediate rendering. Here are the animatable properties, according to the official documentation. Animating closures are called from UIView s static methods. For example, the following code changes the alpha property of adressetextfield during 0.5 seconds to the value 0:
UIView.animate(withDuration: 0.5, animations: { self.adressetextfield.alpha = 0 ) And again, as closure is the last argument, we can use the trailing syntax: UIView.animate(withDuration: 0.5) { self.adressetextfield.alpha = 0 The next example illustrates the use of a completion handler, which execute a second closure once animation is complete. UIView.animate(withDuration: 0.5, animations: { self.adressetextfield.alpha = 0, completion: { _ in print("addresstextfield is now hidden") ) Let s change the design of the view. Here is just a suggestion: - From Storyboard, set our UIMapView to fit the entire size of the screen; extend the UILabel, UISlider and UITextField to the entire width; put them on top of the map; set a light transparency to the UILabel and UITextField; UITextField is just below the UILabel, UISlider is between them; UISwitch button is on top-right of the screen, on top of the UILabel. - Add an animation on addresstextfield to make it visible only when the UISwitch button is off. A suggestion is to make it move to the left until disappearance.. We now focus on the map animation; we want mymapview to rotate based on the user s movements. For this purpose, we will use the heading sensor, which is also managed by CoreLocation framework. Methods locationmanager(startupdatingheading:) and stopupdatingheading() operate similarly as start/stop updating location functions: they ask locationmanager to get new heading value, which can be grabbed by locationmanager(didupdateheading:.
- Find these methods in the documentation. - Add the start/stop heading sensor simultaneously with the start/stop location sensor. - Add the delegate method that get update heading. Note that we obtain an object of class CLHeading whose trueheading property is particularly interesting. In order to rotate the map with the direction of the user, we will apply an affine transformation on our object MKMapView (remember that transform property is animatable). An affine transformation object (structure) is available from CoreGraphics framework, its name then starts by CG - Look in the documentation the transform property. Found it? It s a property of the UIView class (which inherits MKMapView) of type CGAffineTransform. Browse the reference to find the various initializers which define any affine transformation. There is one interesting to make rotation. - From StoryBoard, resize the map so that it is three times the size (width and height) and centered on the center of the view. So when we apply a rotation, the map will always appear in "full screen". - In the delegate function locationmanager(didupdateheading:), change the mapview s transform property to make it rotate. Beware, trueheading is in degree while CGAffineTransform uses radian. - As transform is an animatable property, you could add an animation closure to make the rotation more natural. Finally, let s tell Xcode to disable both status bar (the small bar at top of screen) and autorotation (by default, autorotation is enabled to switch between portrait and landscape modes). Override the following properties in ViewController (as you see they are computed properties ): override var prefersstatusbarhidden: Bool { return true override var shouldautorotate: Bool { return false Great, we used our first animation and our app looks great! Of course, to test this feature, you must use a real device, do not try to rotate your computer ;-) Notice that we designed our screen to one single screen s size, which is define in bottom of Storyboard ( View as: section). For a single screen, you must then arrange your UIObjects according to your preferred device (and/or simulator). For multiple screens compatibility, Xcode uses AutoLayout functionality (working with constraints), which is out of the scope here