From 82f94e2460f1dd9d04ddc38ef6785d41583906da Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 12 Nov 2015 03:33:00 -0600 Subject: [PATCH] Updated articles for Swift 2.0. - MKLocalSearch - MKTileOverlay, MKSnapshotter, and MKDirections - MKGeodesicPolyline - JavaScriptCore --- 2013-04-29-mklocalsearch.md | 9 +- ...leoverlay-mkmapsnapshotter-mkdirections.md | 192 +++++++++++++++++- 2014-04-28-mkgeodesicpolyline.md | 115 ++++++++++- 2015-01-19-javascriptcore.md | 18 +- 4 files changed, 314 insertions(+), 20 deletions(-) diff --git a/2013-04-29-mklocalsearch.md b/2013-04-29-mklocalsearch.md index 8dbc21ed..75b28061 100644 --- a/2013-04-29-mklocalsearch.md +++ b/2013-04-29-mklocalsearch.md @@ -4,7 +4,8 @@ author: Mattt Thompson category: Cocoa excerpt: "In all of the hubbub of torch burning and pitchfork raising, you may have completely missed a slew of additions to MapKit in iOS 6.1." status: - swift: 1.1 + swift: 2.0 + reviewed: November 12, 2015 --- Look, we get it: people are upset about Apple Maps. @@ -26,12 +27,16 @@ request.region = mapView.region let search = MKLocalSearch(request: request) search.startWithCompletionHandler { (response, error) in + guard let response = response else { + print("Search error: \(error)") + return + } + for item in response.mapItems { // ... } } ~~~ - ~~~{objective-c} MKLocalSearchRequest *request = [[MKLocalSearchRequest alloc] init]; request.naturalLanguageQuery = @"Restaurants"; diff --git a/2014-02-03-mktileoverlay-mkmapsnapshotter-mkdirections.md b/2014-02-03-mktileoverlay-mkmapsnapshotter-mkdirections.md index 4d40ae70..9b6a1063 100644 --- a/2014-02-03-mktileoverlay-mkmapsnapshotter-mkdirections.md +++ b/2014-02-03-mktileoverlay-mkmapsnapshotter-mkdirections.md @@ -4,7 +4,8 @@ author: Mattt Thompson category: Cocoa excerpt: "Unless you work with MKMapView. on a regular basis, the last you may have heard about the current state of cartography on iOS may not have been under the cheeriest of circumstances. Therefore, it may come as a surprise maps on iOS have gotten quite a bit better in the intervening releases. Quite good, in fact." status: - swift: t.b.c. + swift: 2.0 + reviewed: November 12, 2015 --- Unless you work with `MKMapView` on a regular basis, the last you may have heard about the current state of cartography on iOS may not have been [under the cheeriest of circumstances](http://www.apple.com/letter-from-tim-cook-on-maps/). Even now, years after the ire of armchair usability experts has moved on to iOS 7's distinct "look and feel", the phrase "Apple Maps" still does not inspire confidence in the average developer. @@ -23,6 +24,14 @@ Don't like the default Apple Maps tiles? [`MKTileOverlay`](https://developer.app ### Setting Custom Map View Tile Overlay +~~~{swift} +let template = "http://tile.openstreetmap.org/{z}/{x}/{y}.png" + +let overlay = MKTileOverlay(URLTemplate: template) +overlay.canReplaceMapContent = true + +mapView.addOverlay(overlay, level: .AboveLabels) +~~~ ~~~{objective-c} static NSString * const template = @"http://tile.openstreetmap.org/{z}/{x}/{y}.png"; @@ -62,6 +71,17 @@ After setting `canReplaceMapContent` to `YES`, the overlay is added to the `MKMa In the map view's delegate, `mapView:rendererForOverlay:` is implemented simply to return a new `MKTileOverlayRenderer` instance when called for the `MKTileOverlay` overlay. +~~~{swift} +// MARK: MKMapViewDelegate + +func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer { + guard let tileOverlay = overlay as? MKTileOverlay else { + return MKOverlayRenderer() + } + + return MKTileOverlayRenderer(tileOverlay: tileOverlay) +} +~~~ ~~~{objective-c} #pragma mark - MKMapViewDelegate @@ -82,6 +102,33 @@ In the map view's delegate, `mapView:rendererForOverlay:` is implemented simply If you need to accommodate a different tile coordinate scheme with your server, or want to add in-memory or offline caching, this can be done by subclassing `MKTileOverlay` and overriding `-URLForTilePath:` and `-loadTileAtPath:result:`: +~~~{swift} +class MKHipsterTileOverlay : MKTileOverlay { + let cache = NSCache() + let operationQueue = NSOperationQueue() + + override func URLForTilePath(path: MKTileOverlayPath) -> NSURL { + return NSURL(string: String(format: "http://tile.example.com/%d/%d/%d", path.z, path.x, path.y))! + } + + override func loadTileAtPath(path: MKTileOverlayPath, result: (NSData?, NSError?) -> Void) { + let url = URLForTilePath(path) + if let cachedData = cache.objectForKey(url) as? NSData { + result(cachedData, nil) + } else { + let request = NSURLRequest(URL: url) + NSURLConnection.sendAsynchronousRequest(request, queue: operationQueue) { + [weak self] + response, data, error in + if let data = data { + self?.cache.setObject(data, forKey: url) + } + result(data, error) + } + } + } +} +~~~ ~~~{objective-c} @interface XXTileOverlay : MKTileOverlay @property NSCache *cache; @@ -123,6 +170,25 @@ Another addition to iOS 7 was [`MKMapSnapshotter`](https://developer.apple.com/l ### Creating a Map View Snapshot +~~~{swift} +let options = MKMapSnapshotOptions() +options.region = mapView.region +options.size = mapView.frame.size +options.scale = UIScreen.mainScreen().scale + +let fileURL = NSURL(fileURLWithPath: "path/to/snapshot.png") + +let snapshotter = MKMapSnapshotter(options: options) +snapshotter.startWithCompletionHandler { snapshot, error in + guard let snapshot = snapshot else { + print("Snapshot error: \(error)") + return + } + + let data = UIImagePNGRepresentation(snapshot.image) + data?.writeToURL(fileURL, atomically: true) +} +~~~ ~~~{objective-c} MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init]; options.region = self.mapView.region; @@ -154,6 +220,36 @@ However, this only draws the map for the specified region; annotations are rende Including annotations—or indeed, any additional information to the map snapshot—can be done by dropping down into Core Graphics: +~~~{swift} +snapshotter.startWithQueue(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { snapshot, error in + guard let snapshot = snapshot else { + print("Snapshot error: \(error)") + fatalError() + } + + let pin = MKPinAnnotationView(annotation: nil, reuseIdentifier: nil) + let image = snapshot.image + + UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale) + image.drawAtPoint(CGPoint.zero) + + let visibleRect = CGRect(origin: CGPoint.zero, size: image.size) + for annotation in mapView.annotations { + var point = snapshot.pointForCoordinate(annotation.coordinate) + if visibleRect.contains(point) { + point.x = point.x + pin.centerOffset.x - (pin.bounds.size.width / 2) + point.y = point.y + pin.centerOffset.y - (pin.bounds.size.height / 2) + pin.image?.drawAtPoint(point) + } + } + + let compositeImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + let data = UIImagePNGRepresentation(compositeImage) + data?.writeToURL(fileURL, atomically: true) +} +~~~ ~~~{objective-c} [snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) completionHandler:^(MKMapSnapshot *snapshot, NSError *error) { @@ -203,12 +299,104 @@ Building on the previous example, here is how `MKDirections` might be used to cr ### Getting Snapshots for each Step of Directions on a Map View +~~~{swift} +let request = MKDirectionsRequest() +request.source = MKMapItem.mapItemForCurrentLocation() +request.destination = MKMapItem(...) + +let directions = MKDirections(request: request) +directions.calculateDirectionsWithCompletionHandler { response, error in + guard let response = response else { + print("Directions error: \(error)") + return + } + + stepImagesFromDirectionsResponse(response) { stepImages in + stepImages.first + print(stepImages) + } +} + +func stepImagesFromDirectionsResponse(response: MKDirectionsResponse, completionHandler: ([UIImage]) -> Void) { + guard let route = response.routes.first else { + completionHandler([]) + return + } + + var stepImages: [UIImage?] = Array(count: route.steps.count, repeatedValue: nil) + var stepImageCount = 0 + + for (index, step) in route.steps.enumerate() { + let snapshotter = MKMapSnapshotter(options: options) + snapshotter.startWithCompletionHandler { snapshot, error in + ++stepImageCount + + guard let snapshot = snapshot else { + print("Snapshot error: \(error)") + return + } + + let image = snapshot.image + + UIGraphicsBeginImageContextWithOptions(image.size, true, image.scale) + image.drawAtPoint(CGPoint.zero) + + // draw the path + guard let c = UIGraphicsGetCurrentContext() else { return } + CGContextSetStrokeColorWithColor(c, UIColor.blueColor().CGColor) + CGContextSetLineWidth(c, 3) + CGContextBeginPath(c) + + var coordinates: UnsafeMutablePointer = UnsafeMutablePointer.alloc(step.polyline.pointCount) + defer { coordinates.dealloc(step.polyline.pointCount) } + + step.polyline.getCoordinates(coordinates, range: NSRange(location: 0, length: step.polyline.pointCount)) + + for i in 0 ..< step.polyline.pointCount { + let p = snapshot.pointForCoordinate(coordinates[i]) + if i == 0 { + CGContextMoveToPoint(c, p.x, p.y) + } else { + CGContextAddLineToPoint(c, p.x, p.y) + } + } + + CGContextStrokePath(c) + + // add the start and end points + let visibleRect = CGRect(origin: CGPoint.zero, size: image.size) + + for mapItem in [response.source, response.destination] + where mapItem.placemark.location != nil { + var point = snapshot.pointForCoordinate(mapItem.placemark.location!.coordinate) + if CGRectContainsPoint(visibleRect, point) { + let pin = MKPinAnnotationView(annotation: nil, reuseIdentifier: nil) + pin.pinTintColor = mapItem.isEqual(response.source) ? + MKPinAnnotationView.greenPinColor() : MKPinAnnotationView.redPinColor() + point.x = point.x + pin.centerOffset.x - (pin.bounds.size.width / 2) + point.y = point.y + pin.centerOffset.y - (pin.bounds.size.height / 2) + pin.image?.drawAtPoint(point) + } + } + + let stepImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + stepImages[index] = stepImage + + if stepImageCount == stepImages.count { + completionHandler(stepImages.flatMap({ $0 })) + } + } + } +} +~~~ ~~~{objective-c} NSMutableArray *mutableStepImages = [NSMutableArray array]; MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init]; request.source = [MKMapItem mapItemForCurrentLocation]; -request.destination = nil;//...; +request.destination = ...; MKDirections *directions = [[MKDirections alloc] initWithRequest:request]; [directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) { diff --git a/2014-04-28-mkgeodesicpolyline.md b/2014-04-28-mkgeodesicpolyline.md index 46ab68eb..0e152fca 100644 --- a/2014-04-28-mkgeodesicpolyline.md +++ b/2014-04-28-mkgeodesicpolyline.md @@ -4,7 +4,8 @@ author: Mattt Thompson category: Cocoa excerpt: "We knew that the Earth was not flat long before 1492. Early navigators observed the way ships would dip out of view over the horizon many centuries before the Age of Discovery. For many iOS developers, though, a flat MKMapView was a necessary conceit until recently." status: - swift: t.b.c. + swift: 2.0 + reviewed: November 12, 2015 --- We knew that the Earth was not flat long before 1492. Early navigators observed the way ships would dip out of view over the horizon many centuries before the Age of Discovery. @@ -23,6 +24,15 @@ An `MKGeodesicPolyline` is created with an array of 2 `MKMapPoint`s or `CLLocati ### Creating an `MKGeodesicPolyline` +~~~{swift} +let LAX = CLLocation(latitude: 33.9424955, longitude: -118.4080684) +let JFK = CLLocation(latitude: 40.6397511, longitude: -73.7789256) + +var coordinates = [LAX.coordinate, JFK.coordinate] +let geodesicPolyline = MKGeodesicPolyline(coordinates: &coordinates, count: 2) + +mapView.addOverlay(geodesicPolyline) +~~~ ~~~{objective-c} CLLocation *LAX = [[CLLocation alloc] initWithLatitude:33.9424955 longitude:-118.4080684]; @@ -41,14 +51,33 @@ MKGeodesicPolyline *geodesicPolyline = Although the overlay looks like a smooth curve, it is actually comprised of thousands of tiny line segments (true to its `MKPolyline` lineage): +~~~{swift} +print(geodesicPolyline.pointCount) // 3984 +~~~ ~~~{objective-c} -NSLog(@"%d", geodesicPolyline.pointsCount) // 3984 +NSLog(@"%d", geodesicPolyline.pointCount) // 3984 ~~~ Like any object conforming to the `` protocol, an `MKGeodesicPolyline` instance is displayed by adding it to an `MKMapView` with `-addOverlay:` and implementing `mapView:rendererForOverlay:`: ### Rendering `MKGeodesicPolyline` on an `MKMapView` +~~~{swift} +// MARK: MKMapViewDelegate + +func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer { + guard let polyline = overlay as? MKPolyline else { + return MKOverlayRenderer() + } + + let renderer = MKPolylineRenderer(polyline: polyline) + renderer.lineWidth = 3.0 + renderer.alpha = 0.5 + renderer.strokeColor = UIColor.blueColor() + + return renderer +} +~~~ ~~~{objective-c} #pragma mark - MKMapViewDelegate @@ -85,6 +114,13 @@ Since geodesics make reasonable approximations for flight paths, a common use ca To do this, we'll make properties for our map view and geodesic polyline between LAX and JFK, and add new properties for the `planeAnnotation` and `planeAnnotationPosition` (the index of the current map point for the polyline): +~~~{swift} +// MARK: Flight Path Properties +var mapView: MKMapView! +var flightpathPolyline: MKGeodesicPolyline! +var planeAnnotation: MKPointAnnotation! +var planeAnnotationPosition = 0 +~~~ ~~~{objective-c} @interface MapViewController () @property MKMapView *mapView; @@ -96,6 +132,14 @@ To do this, we'll make properties for our map view and geodesic polyline between Next, right below the initialization of our map view and polyline, we create an `MKPointAnnotation` for our plane: +~~~{swift} +let annotation = MKPointAnnotation() +annotation.title = NSLocalizedString("Plane", comment: "Plane marker") +mapView.addAnnotation(annotation) + +self.planeAnnotation = annotation +self.updatePlanePosition() +~~~ ~~~{objective-c} self.planeAnnotation = [[MKPointAnnotation alloc] init]; self.planeAnnotation.title = NSLocalizedString(@"Plane", nil); @@ -106,6 +150,21 @@ self.planeAnnotation.title = NSLocalizedString(@"Plane", nil); That call to `updatePlanePosition` in the last line ticks the animation and updates the position of the plane: +~~~{swift} +func updatePlanePosition() { + let step = 5 + guard planeAnnotationPosition + step < flightpathPolyline.pointCount + else { return } + + let points = flightpathPolyline.points() + self.planeAnnotationPosition += step + let nextMapPoint = points[planeAnnotationPosition] + + self.planeAnnotation.coordinate = MKCoordinateForMapPoint(nextMapPoint) + + performSelector("updatePlanePosition", withObject: nil, afterDelay: 0.03) +} +~~~ ~~~{objective-c} - (void)updatePlanePosition { static NSUInteger const step = 5; @@ -123,11 +182,22 @@ That call to `updatePlanePosition` in the last line ticks the animation and upda } ~~~ - We'll perform this method roughly 30 times a second, until the plane has arrived at its final destination. Finally, we implement `mapView:viewForAnnotation:` to have the annotation render on the map view: +~~~{swift} +func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? { + let planeIdentifier = "Plane" + + let annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(planeIdentifier) + ?? MKAnnotationView(annotation: annotation, reuseIdentifier: planeIdentifier) + + annotationView.image = UIImage(named: "airplane") + + return annotationView +} +~~~ ~~~{objective-c} - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation @@ -137,7 +207,7 @@ Finally, we implement `mapView:viewForAnnotation:` to have the annotation render MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:PinIdentifier]; if (!annotationView) { annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:PinIdentifier]; - }; + } annotationView.image = [UIImage imageNamed:@"plane"]; @@ -155,6 +225,14 @@ Let's update the rotation of the plane as it moves across its flightpath. To calculate the plane's direction, we'll take the slope from the previous and next points: +~~~{swift} +let previousMapPoint = points[planeAnnotationPosition] +planeAnnotationPosition += step +let nextMapPoint = points[planeAnnotationPosition] + +self.planeDirection = directionBetweenPoints(previousMapPoint, nextMapPoint) +self.planeAnnotation.coordinate = MKCoordinateForMapPoint(nextMapPoint) +~~~ ~~~{objective-c} MKMapPoint previousMapPoint = self.flightpathPolyline.points[self.planeAnnotationPosition]; self.planeAnnotationPosition += step; @@ -164,10 +242,18 @@ self.planeDirection = XXDirectionBetweenPoints(previousMapPoint, nextMapPoint); self.planeAnnotation.coordinate = MKCoordinateForMapPoint(nextMapPoint); ~~~ -`XXDirectionBetweenPoints` is a function that returns a `CLLocationDirection` (0 – 360 degrees, where North = 0) given two `MKMapPoint`s. +`directionBetweenPoints` is a function that returns a `CLLocationDirection` (0 – 360 degrees, where North = 0) given two `MKMapPoint`s. > We calculate from `MKMapPoint`s rather than converted coordinates, because we're interested in the slope of the line on the flat projection. +~~~{swift} +private func directionBetweenPoints(sourcePoint: MKMapPoint, _ destinationPoint: MKMapPoint) -> CLLocationDirection { + let x = destinationPoint.x - sourcePoint.x + let y = destinationPoint.y - sourcePoint.y + + return radiansToDegrees(atan2(y, x)) % 360 + 90 +} +~~~ ~~~{objective-c} static CLLocationDirection XXDirectionBetweenPoints(MKMapPoint sourcePoint, MKMapPoint destinationPoint) { double x = destinationPoint.x - sourcePoint.x; @@ -177,8 +263,17 @@ static CLLocationDirection XXDirectionBetweenPoints(MKMapPoint sourcePoint, MKMa } ~~~ -That convenience function `XXRadiansToDegrees` (and its partner, `XXDegreesToRadians`) are simply: +That convenience function `radiansToDegrees` (and its partner, `degreesToRadians`) are simply: + +~~~{swift} +private func radiansToDegrees(radians: Double) -> Double { + return radians * 180 / M_PI +} +private func degreesToRadians(degrees: Double) -> Double { + return degrees * M_PI / 180 +} +~~~ ~~~{objective-c} static inline double XXRadiansToDegrees(double radians) { return radians * 180.0f / M_PI; @@ -189,10 +284,14 @@ static inline double XXDegreesToRadians(double degrees) { } ~~~ -That direction is stored in a new property, `@property CLLocationDirection planeDirection;`, calculated from `self.planeDirection = XXDirectionBetweenPoints(currentMapPoint, nextMapPoint);` in `updatePlanePosition` (ideally renamed to `updatePlanePositionAndDirection` with this addition). To make the annotation rotate, we apply a `transform` on `annotationView`: +That direction is stored in a new property, `var planeDirection: CLLocationDirection = 0`, calculated from `self.planeDirection = directionBetweenPoints(currentMapPoint, nextMapPoint)` in `updatePlanePosition` (ideally renamed to `updatePlanePositionAndDirection` with this addition). To make the annotation rotate, we apply a `transform` on `annotationView`: +~~~{swift} +annotationView.transform = CGAffineTransformRotate(mapView.transform, + degreesToRadians(planeDirection)) +~~~ ~~~{objective-c} -annotationView.transform = +self.annotationView.transform = CGAffineTransformRotate(self.mapView.transform, XXDegreesToRadians(self.planeDirection)); ~~~ diff --git a/2015-01-19-javascriptcore.md b/2015-01-19-javascriptcore.md index 1992b3e3..241e7a97 100644 --- a/2015-01-19-javascriptcore.md +++ b/2015-01-19-javascriptcore.md @@ -301,19 +301,21 @@ All that remains is to load the JSON data, call into the `JSContext` to parse th ````swift // get JSON string -guard let peopleJSON = try? String(contentsOfFile: ..., encoding: NSUTF8StringEncoding) - else { return } +let peopleJSON = try! String(contentsOfFile: ..., encoding: NSUTF8StringEncoding) // get load function let load = context.objectForKeyedSubscript("loadPeopleFromJSON") // call with JSON and convert to an Array -guard let people = load.callWithArguments([peopleJSON]).toArray() as? [Person] - else { return } +if let people = load.callWithArguments([peopleJSON]).toArray() as? [Person] { -let template = "{% raw %}{{getFullName}}, born {{birthYear}}{% endraw %}" -let mustacheRender = context.objectForKeyedSubscript("Mustache").objectForKeyedSubscript("render") -for person in people { - print(mustacheRender.callWithArguments([template, person])) + // get rendering function and create template + let mustacheRender = context.objectForKeyedSubscript("Mustache").objectForKeyedSubscript("render") + let template = "{% raw %}{{getFullName}}, born {{birthYear}}{% endraw %}" + + // loop through people and render Person object as string + for person in people { + print(mustacheRender.callWithArguments([template, person])) + } } // Output: