内容简介:While MapKit makes it easy to add a map to your app, that alone isn’t very engaging. Fortunately, you can useIn this MapKit tutorial, you’ll create an app that showcases
Update note : Rony Rozen updated this tutorial for Xcode 11 and Swift 5. Chris Wagner and Owen Brown wrote the previous versions of this tutorial.
While MapKit makes it easy to add a map to your app, that alone isn’t very engaging. Fortunately, you can use custom overlay views to make more appealing maps.
In this MapKit tutorial, you’ll create an app that showcases Six Flags Magic Mountain . By the time you’re done, you’ll have an interactive park map that shows attraction locations, ride routes and character locations. This app is for all you fast-ride thrill seekers out there. ;]
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Then, open the project in Xcode.
The starter project includes the map you’ll work with and buttons to toggle the different types of overlays on and off.
Build and run. You’ll see something like this:
Note : If you feel like you need a refresher on how to add a map to your app, or want to dive deeper on the basics of working with MapKit, visit MapKit Tutorial: Getting Started .
Once you feel ready, dive right into overlay views.
All About Overlay Views
Before you start creating overlay views, you need to understand two key classes: MKOverlay
and MKOverlayRenderer
.
MKOverlay
tells MapKit where you want it to draw the overlays. There are three steps for using this class:
- First, create your custom class that implements the
MKOverlay
protocol , which has two required properties:coordinate
andboundingMapRect
. These properties define where the overlay resides on the map and its size. - Then, create an instance of your class for each area where you want to display an overlay. In this app, for example, you’ll create an instance for a roller coaster overlay and another for a restaurant overlay.
- Finally, add the overlays to your map view.
At this point, the map knows where it’s supposed to display the overlays. But it doesn’t know what to display in each region.
This is where MKOverlayRenderer
comes in. Subclassing it lets you set up what you want to display in each spot.
For example, in this app, you’ll draw an image of the roller coaster or restaurant. MapKit expects to present a MKMapView
object, and this class defines the drawing infrastructure used by the map view.
Look at the starter project. In ContentView.swift , you’ll see a delegate method that lets you return an overlay view:
func mapView( _ mapView: MKMapView, rendererFor overlay: MKOverlay ) -> MKOverlayRenderer
MapKit calls this method when it realizes there’s an MKOverlay
object in the region the map view is displaying.
To sum up, you don’t add MKOverlayRenderer
objects directly to the map view. Instead, you tell the map about MKOverlay
objects to display and return MKOverlayRenderer
s when the delegate method requests them.
Now that you’ve covered the theory, it’s time to put these concepts to use!
Adding Your Information to the Map
Currently, the map doesn’t provide enough information about the park. Your task is to create an object that represents an overlay for the entire park.
First, select the Overlays group and create a new Swift file named ParkMapOverlay.swift . Then replace its contents with:
import MapKit class ParkMapOverlay: NSObject, MKOverlay { let coordinate: CLLocationCoordinate2D let boundingMapRect: MKMapRect init(park: Park) { boundingMapRect = park.overlayBoundingMapRect coordinate = park.midCoordinate } }
Conforming to MKOverlay
forces you to inherit from NSObject
. The initializer takes the properties from the passed Park
object, which is already in the starter project, and sets them to the corresponding MKOverlay
properties.
Next, you need to create a MKOverlayRenderer
that knows how to draw this overlay.
Create a new Swift file in the Overlays group called ParkMapOverlayView.swift . Replace its contents with:
import MapKit class ParkMapOverlayView: MKOverlayRenderer { let overlayImage: UIImage // 1 init(overlay: MKOverlay, overlayImage: UIImage) { self.overlayImage = overlayImage super.init(overlay: overlay) } // 2 override func draw( _ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext ) { guard let imageReference = overlayImage.cgImage else { return } let rect = self.rect(for: overlay.boundingMapRect) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: 0.0, y: -rect.size.height) context.draw(imageReference, in: rect) } }
Here’s a breakdown of what you added:
-
init(overlay:overlayImage:)
overrides the base methodinit(overlay:)
by providing a second argument. -
draw(_:zoomScale:in:)
is the real meat of this class. It defines how MapKit should render this view when given a specificMKMapRect
,MKZoomScale
and theCGContext
of the graphic context, with the intent to draw the overlay image onto the context at the appropriate scale.
Note : The details of Core Graphics drawing are outside the scope of this tutorial. However, you can see the code above uses the passed MKMapRect
to get a CGRect
in which to draw the image in the provided context. To learn more about Core Graphics, check out the Core Graphics tutorial series .
Great! Now that you have both an MKOverlay
and MKOverlayRenderer
, add them to your map view.
Creating Your First Map Overlay
In ContentView.swift , find addOverlay()
and change its TODO content to:
let overlay = ParkMapOverlay(park: park) mapView.addOverlay(overlay)
This method adds an ParkMapOverlay
to the map view.
Take a look at updateMapOverlayViews()
. You’ll see when a user taps the button in the navigation bar to show the map overlay, addOverlay()
is called. Now that you’ve added the necessary code, the overlay displays.
Notice that updateMapOverlayViews()
also removes any annotations and overlays that may be present so you don’t end up with duplicate renderings. This is not necessarily efficient, but it’s a simple approach to clear previous items from the map.
The last step standing between you and seeing your newly implemented overlay on the map is mapView(_:rendererFor:)
, mentioned earlier. Replace its current TODO implementation with:
if overlay is ParkMapOverlay { return ParkMapOverlayView( overlay: overlay, overlayImage: UIImage(imageLiteralResourceName: "overlay_park")) }
When MapKit determines an MKOverlay
is in view, it calls this delegate method to obtain a renderer.
Here, you check if the overlay is of class type ParkMapOverlay
. If so, you load the overlay image, create a ParkMapOverlayView
instance with the overlay image and return this instance to the caller.
There’s one little piece missing, though: Where does that suspicious little overlay_park
image come from? It’s a PNG to overlay the map with the defined park’s boundary. The overlay_park
image, found in Assets.xcassets , looks like this:
Build and run, enable the :Overlay: option at the top of the screen and voilà! Here’s the park overlay drawn on top of your map:
Zoom in, zoom out, and move around. The overlay scales and moves as you would expect. Cool!
Adding Annotations
If you’ve ever searched for a location in the native Maps app, you’ve seen those colored pins appear on the map. These are annotations created with MKAnnotationView
. You can use annotations in your own app and use any image you want, not just pins!
Annotations are useful to help highlight specific points of interest to park visitors. They work similarly to MKOverlay
and MKOverlayRenderer
, but instead you’ll work with MKAnnotation
and MKAnnotationView
.
Writing Your First Annotation
First, create a new Swift file in the Annotations group called AttractionAnnotation.swift . Then, replace its contents with:
import MapKit // 1 enum AttractionType: Int { case misc = 0 case ride case food case firstAid func image() -> UIImage { switch self { case .misc: return UIImage(imageLiteralResourceName: "star") case .ride: return UIImage(imageLiteralResourceName: "ride") case .food: return UIImage(imageLiteralResourceName: "food") case .firstAid: return UIImage(imageLiteralResourceName: "firstaid") } } } // 2 class AttractionAnnotation: NSObject, MKAnnotation { // 3 let coordinate: CLLocationCoordinate2D let title: String? let subtitle: String? let type: AttractionType // 4 init( coordinate: CLLocationCoordinate2D, title: String, subtitle: String, type: AttractionType ) { self.coordinate = coordinate self.title = title self.subtitle = subtitle self.type = type } }
Here’s what you added:
-
AttractionType
helps you categorize each attraction into a type. This enum lists four types of annotations: misc, rides, foods and first aid. There’s also a handy method to grab the correct annotation image. - You create this class and make it conform to
MKAnnotation
. - Much like
MKOverlay
,MKAnnotation
has a requiredcoordinate
property. You define a handful of properties specific to this implementation. - Finally, you define an initializer that lets you assign values to each of the properties.
Next, you’ll create a specific instance of MKAnnotationView
to use for your annotations.
Associating a View With Your Annotation
First, create another Swift file called AttractionAnnotationView.swift in Annotations group. Then, replace its contents with the following snippet:
import MapKit class AttractionAnnotationView: MKAnnotationView { // 1 // Required for MKAnnotationView required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } // 2 override init(annotation: MKAnnotation?, reuseIdentifier: String?) { super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) guard let attractionAnnotation = self.annotation as? AttractionAnnotation else { return } image = attractionAnnotation.type.image() } }
Here’s a code breakdown:
-
MKAnnotationView
requiresinit(coder:)
. Without its definition, an error prevents you from building and running the app. To avoid this, define it and call its superclass initializer. - You also override
init(annotation:reuseIdentifier:)
and set a different annotation image based on the annotation’s type property.
Now that you’ve created the annotation and its associated view, you can start adding annotations to your map view!
Adding Annotations to the Map
To determine the location of each annotation, you’ll use the info in the MagicMountainAttractions.plist file, which you can find under the Park Information group. The plist file contains coordinate information and other details about the attractions at the park.
Go back to ContentView.swift and replace the TODO: implementation of addAttractionPins()
with:
// 1 guard let attractions = Park.plist("MagicMountainAttractions") as? [[String: String]] else { return } // 2 for attraction in attractions { let coordinate = Park.parseCoord(dict: attraction, fieldName: "location") let title = attraction["name"] ?? "" let typeRawValue = Int(attraction["type"] ?? "0") ?? 0 let type = AttractionType(rawValue: typeRawValue) ?? .misc let subtitle = attraction["subtitle"] ?? "" // 3 let annotation = AttractionAnnotation( coordinate: coordinate, title: title, subtitle: subtitle, type: type) mapView.addAnnotation(annotation) }
Here’s a step by step breakdown:
- First, you read MagicMountainAttractions.plist and store it as an array of dictionaries.
- Then, you iterate over each dictionary in the array.
- For each entry, you create an instance of
AttractionAnnotation
with the point’s information and add it to the map view.
You’re almost there! Last but not least, you need to implement another delegate method that provides the MKAnnotationView
instances to the map view so it can render them on itself.
Add the following method to the Coordinator
class at the top of the file:
func mapView( _ mapView: MKMapView, viewFor annotation: MKAnnotation ) -> MKAnnotationView? { let annotationView = AttractionAnnotationView( annotation: annotation, reuseIdentifier: "Attraction") annotationView.canShowCallout = true return annotationView }
This method receives the selected MKAnnotation
and uses it to create the AttractionAnnotationView
. Since the property canShowCallout
is set to true
, a call-out appears when the user touches the annotation. Finally, the method returns the annotation view.
Build and run to see your annotations in action! Don’t forget to turn on the :Pins: option.
The Attraction pins are looking rather sharp at this point! :]
So far, you’ve covered several complicated bits of MapKit, including overlays and annotations. But what if you need to use some drawing primitives, like lines and circles?
The MapKit framework also lets you draw directly onto a map view. MapKit provides MKPolyline
, MKPolygon
and MKCircle
for this purpose. Time to try them out!
I Walk The Line: MKPolyline
If you’ve ever been to Magic Mountain, you know that the Goliath hypercoaster is an incredible ride. Some riders like to make a beeline for it as soon as they walk in the gate! :]
To help these riders out, you’ll plot a path from the entrance of the park straight to the Goliath.
MKPolyline
is a great solution for drawing a path that connects multiple points, such as plotting a non-linear route from point A to point B.
To draw a polyline, you need a series of longitude and latitude coordinates in the order they should be plotted. EntranceToGoliathRoute.plist , again found in the Park Information folder, contains the path information.
Now, you need a way to read that plist file and create the route for the riders to follow.
First, open ContentView.swift and find addRoute()
. Then, replace it’s current TODO implementation with:
guard let points = Park.plist("EntranceToGoliathRoute") as? [String] else { return } let cgPoints = points.map { NSCoder.cgPoint(for: $0) } let coords = cgPoints.map { CLLocationCoordinate2D( latitude: CLLocationDegrees($0.x), longitude: CLLocationDegrees($0.y)) } let myPolyline = MKPolyline(coordinates: coords, count: coords.count) mapView.addOverlay(myPolyline)
This method reads EntranceToGoliathRoute.plist and converts the individual coordinate strings to CLLocationCoordinate2D
structures.
It’s remarkable how simple it is to implement a polyline: You simply create an array containing all of the points and pass it to MKPolyline
! It doesn’t get much easier than that.
Remember, updateMapOverlayViews()
already calls addRoute()
whenever the user toggles this option via the UI. All that’s left now is for you to update the delegate method so it returns the actual view you want to render on the map view.
Go back to mapView(_:rendererFor:)
and add this else if
clause to the existing condition:
else if overlay is MKPolyline { let lineView = MKPolylineRenderer(overlay: overlay) lineView.strokeColor = .green return lineView }
The process of displaying the polyline view is very similar to previous overlay views. However, in this case, you don’t need to create any custom view objects. You simply use the provided MKPolyLineRenderer
class and initialize a new instance with the overlay.
MKPolyLineRenderer
also lets you change some of the polyline’s attributes. In this case, you’ve modified the stroke color to show as green.
Build and run your app. Enable the :Route: option and it’ll appear on the screen:
Now Goliath fanatics can make it to the coaster in record time! :]
It would be nice to show the park patrons where the park boundaries are, since the park doesn’t actually occupy the entire space shown on the screen.
You could use MKPolyline
to draw a shape around the park boundaries, but MapKit provides another class specifically designed to draw closed polygons: MKPolygon
.
Don’t Fence Me In: MKPolygon
MKPolygon
is similar to MKPolyline
, except the first and last points in the set of coordinates are connected to each other to create a closed shape.
You’ll create an MKPolygon
as an overlay that shows the park boundaries. The park boundary coordinates are defined in MagicMountain.plist . Look at init(filename:)
in Park.swift to see where the boundary points are read from the plist file.
Now, in ContentView.swift , replace the TODO implementation of addBoundary()
with:
mapView.addOverlay(MKPolygon( coordinates: park.boundary, count: park.boundary.count))
Given the boundary array and point count from the park instance, you can quickly and easily create a new MKPolygon
instance!
Can you guess the next step here? It’s similar to what you did for MKPolyline
.
Yup, that’s right. MKPolygon
conforms to MKOverlay
like MKPolyline
does, so you need to update the delegate method again.
Go back to mapView(_:rendererFor:)
and add this else if
clause to the existing conditions:
else if overlay is MKPolygon { let polygonView = MKPolygonRenderer(overlay: overlay) polygonView.strokeColor = .magenta return polygonView }
You create an MKOverlayView
as an instance of MKPolygonRenderer
and set the stroke color to magenta.
Run the app and enable the :Bound: option to see your new boundary in action. You may have to zoom out so the park boundaries can fit the simulator’s screen bounds.
That takes care of polylines and polygons. The last drawing method to cover is drawing circles as an overlay, which you’ll do with MKCircle
.
Circle in the Sand: MKCircle
MKCircle
is also very similar to MKPolyline
and MKPolygon
, except it draws a circle when given the center coordinate point and a radius that determines the size of the circle.
Many park visitors like to visit with the characters. You can help them find the characters by marking where they were last spotted on the map with circles. The MKCircle
overlay lets you do this easily.
The Park Information folder also contains the character location files. Each file is an array of a few coordinates where the user spotted characters.
First, create a new swift file under the Models group called Character.swift . Then replace its contents with the following code:
import MapKit // 1 class Character: MKCircle { // 2 private var name: String? var color: UIColor? // 3 convenience init(filename: String, color: UIColor) { guard let points = Park.plist(filename) as? [String] else { self.init() return } let cgPoints = points.map { NSCoder.cgPoint(for: $0) } let coords = cgPoints.map { CLLocationCoordinate2D( latitude: CLLocationDegrees($0.x), longitude: CLLocationDegrees($0.y)) } let randomCenter = coords[Int.random(in: 0...3)] let randomRadius = CLLocationDistance(Int.random(in: 5...39)) self.init(center: randomCenter, radius: randomRadius) self.name = filename self.color = color } }
Here’s what this code does:
- Character class conforms to
MKCircle
protocol. - It defines two optional properties:
name
andcolor
. - The convenience initializer accepts a plist filename and color to draw the circle. Then it reads in the data from the plist file and selects a random location from the four locations in the file. Next, it chooses a random radius to simulate the time variance. The
MKCircle
returned is set and ready to be put on the map!
Now you need a method to add the characters. So, open ContentView.swift and replace the TODO implementation of addCharacterLocation()
with:
mapView.addOverlay(Character(filename: "BatmanLocations", color: .blue)) mapView.addOverlay(Character(filename: "TazLocations", color: .orange)) mapView.addOverlay(Character(filename: "TweetyBirdLocations", color: .yellow))
This method performs pretty much the same operations for each character: It passes the plist filename for each one, decides on a color and adds it to the map as an overlay.
You’re almost done! Do you remember the last step?
Right! You need to give the map view a MKOverlayView
by using the delegate method.
Go back to mapView(_:rendererFor:)
and add this else if
clause to the existing conditions:
else if let character = overlay as? Character { let circleView = MKCircleRenderer(overlay: character) circleView.strokeColor = character.color return circleView }
Build and run the app, and enable the :Characters: option to see where everyone is hiding!
Where to Go From Here?
You can download the complete project by using the Download Materials button at the top or bottom of this tutorial.
Congratulations! You’ve worked with some of the most important capabilities that MapKit provides. With a few basic functions, you’ve implemented a full-blown and practical mapping app complete with annotations and custom overlays.
There are many ways to generate overlays that range from very easy to very complex. The approach taken in this tutorial for the overlay_park
image was the easy, yet tedious, route.
There are much more advanced, and perhaps more efficient, methods to create overlays. For example, you could use KML tiles or other third party provided resources.
To learn more about MapKit, check out the other MapKit tutorials on our site, such as Advanced MapKit Tutorial: Custom Tiles .
I hope you enjoyed this tutorial, and I hope to see you use MapKit overlays in your own apps. If you have any questions or comments, please join the forum discussion below!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
旷世之战――IBM深蓝夺冠之路
纽伯 / 邵谦谦 / 清华大学出版社 / 2004-5 / 35.0
本书作者Monty Neworn是国际计算机象棋协公的主席,作者是用生动活泼的笔触描写了深蓝与卡斯帕罗夫之战这一引起全世界关注的历史事件的前前后后。由于作者的特殊身份和多年来对计算机象棋的关心,使他掌握了许多局外人不能得到的资料,记叙了很多鲜为人知的故事。全书行文流畅、文笔优美,对于棋局的描述更是跌宕起伏、险象环生,让读者好像又一次亲身经历了那场流动人心的战争。 本书作为一本科普读物......一起来看看 《旷世之战――IBM深蓝夺冠之路》 这本书的介绍吧!