Location Awareness Programming Guide
Location Awareness Programming Guide
Contents
Contents
Displaying Maps 29
Understanding Map Geometry 29 Map Coordinate Systems 30 Converting Between Coordinate Systems 31 Adding a Map View to Your User Interface 32 Configuring the Properties of a Map 33 Setting the Visible Portion of the Map 33 Zooming and Panning the Map Content 33 Displaying the Users Current Location on the Map 34 Responding to User Interactions with a Map 35
Annotating Maps 36
Adding Annotations to a Map 37 Checklist for Adding an Annotation to the Map 38 Defining a Custom Annotation Object 39 Using the Standard Annotation Views 40 Defining a Custom Annotation View 41 Creating Annotation Views from Your Delegate Object 43 Managing the Maps Annotation Objects 45 Marking Your Annotation View as Draggable 45 Displaying Overlays on a Map 46 Checklist for Adding an Overlay to the Map 48 Using the Standard Overlay Objects and Views 49 Defining a Custom Overlay Object 50 Defining a Custom Overlay View 51 Creating Overlay Views from Your Delegate Object 54 Managing the Maps Overlay Objects 55 Using Overlays as Annotations 55
Displaying Maps 29
Figure 4-1 Table 4-1 Mapping spherical data to a flat surface 30 Map coordinate system conversion routines 31
Annotating Maps 36
Figure 5-1 Figure 5-2 Figure 5-3 Listing 5-1 Listing 5-2 Listing 5-3 Listing 5-4 Listing 5-5 Listing 5-6 Listing 5-7 Listing 5-8 Listing 5-9 Displaying an annotation in a map 37 Displaying an overlay on a map 47 Using a custom overlay view to draw 54 Creating a simple annotation object 39 Implementing the MyCustomAnnotation class 40 Creating a standard annotation view 41 Declaring a custom annotation view 41 Initializing a custom annotation view 42 Creating annotation views 43 Creating a polygon overlay object 49 Creating a polygon view for rendering a shape 50 Drawing a gradient in a custom overlay view 52
Using location-based information in your applications is a way to keep the user connected to the surrounding world. Whether you use this information for practical purposes (such as navigation) or for entertainment, location-based information can improve the overall user experience.
Location-based information in iOS comprises two pieces: location services and maps. Location services are provided by the Core Location framework, which provides Objective-C interfaces for obtaining information about the users location and heading. Maps are provided by the Map Kit framework, which supports both the display and annotation of maps similar to those found in the Maps application.
At a Glance
Map and location services provide a way for you to enhance user interactions. By incorporating geographic data into your applications, you can orient the user to the surrounding environment or help the user stay connected to other people nearby.
Relevant Chapters Displaying Maps (page 29), Annotating Maps (page 36)
See Also
For information about the classes of the Core Location framework, see Core Location Framework Reference . For information about the classes of the Map Kit framework, see Map Kit Framework Reference .
Applications use location data for a wide variety of purposes, ranging from social networking to turn-by-turn navigation services. They get location data by using the classes of the Core Location framework. This framework provides several services that you can use to get and monitor the devices current location:
The significant-change location service provides a low-power way to get the current location and be notified of changes to that location. (iOS 4.0 and later). The standard location service offers a more configurable way to get the current location. Region monitoring lets you monitor boundary crossings for a defined area. (iOS 4.0 and later).
To use the features of the Core Location framework, you must link your application to CoreLocation.framework in your Xcode project. To access the classes and headers of the framework, include an #import <CoreLocation/CoreLocation.h> statement at the top of any relevant source files. For general information about the classes of the Core Location framework, see Core Location Framework Reference .
Include the location-services string if you require location services in general. Include the gps string if your application requires the accuracy offered only by GPS hardware.
Important If your application uses location services but is able to operate successfully without them, do not include the corresponding strings in the UIRequiredDeviceCapabilities key. For more information about the UIRequiredDeviceCapabilities key, see Information Property List Key Reference .
The standard location service is a configurable, general-purpose solution and is supported in all versions of iOS. The significant-change location service offers a low-power location service for devices with cellular radios. This service is available only in iOS 4.0 and later and can also wake up an application that is suspended or not running.
Gathering location data is a power-intensive operation. It involves powering up the onboard radios and querying the available cell towers, Wi-Fi hotspots, or GPS satellites, which can take several seconds. Leaving the standard location service running for extended periods can drain the devices battery. (The significant-change location service drastically reduces battery drain by monitoring only cell tower changes, but the service works only on devices with cellular radios.) For most applications, it is usually sufficient to establish an initial position fix and then acquire updates only periodically after that. If you are sure you need regular position updates, you should use the significant-change location service where you can; otherwise, you should configure the parameters of the standard location service in a way that minimizes its impact on battery life.
The user can disable location services in the Settings application. The user can deny location services for a specific application. The device might be in Airplane mode and unable to power up the necessary hardware.
10
For these reasons, it is recommended that you always call the locationServicesEnabled class method of CLLocationManager before attempting to start either the standard or significant-change location services. (In iOS 3.x and earlier, check the value of the locationServicesEnabled property instead.) If this class method returns YES, you can start location services as planned. If it returns NO and you attempt to start location services anyway, the system prompts the user to confirm whether location services should be reenabled. Given that location services are very likely to be disabled on purpose, the user might not welcome this prompt.
- (void)startStandardUpdates { // Create the location manager if this object does not // already have one. if (nil == locationManager) locationManager = [[CLLocationManager alloc] init];
11
[locationManager startUpdatingLocation]; }
The code for receiving location updates from this service is shown in Receiving Location Data from a Service (page 13).
- (void)startSignificantChangeUpdates { // Create the location manager if this object does not // already have one. if (nil == locationManager) locationManager = [[CLLocationManager alloc] init];
12
As with the standard location service, location data is delivered to the delegate object as described in Receiving Location Data from a Service (page 13). If you leave this service running and your application is subsequently suspended or terminated, the service automatically wakes up your application when new location data arrives. At wake-up time, your application is put into the background and given a small amount of time to process the location data. Because your application is in the background, it should do minimal work and avoid any tasks (such as querying the network) that might prevent it from returning before the allocated time expires. If it does not, your application may be terminated.
// Delegate method from the CLLocationManagerDelegate protocol. - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation { // If it's a relatively recent event, turn off updates to save power NSDate* eventDate = newLocation.timestamp; NSTimeInterval howRecent = [eventDate timeIntervalSinceNow]; if (abs(howRecent) < 15.0) { NSLog(@"latitude %+.6f, longitude %+.6f\n", newLocation.coordinate.latitude, newLocation.coordinate.longitude);
13
In addition to a location objects timestamp, you can also use the accuracy reported by that object as a means of determining whether you want to accept an event. As it receives more accurate data, the location service may return additional events, with the accuracy values reflecting the improvements accordingly. Throwing away less accurate events means your application wastes less time on events that cannot be used effectively anyway.
The device may not have the hardware needed to support region monitoring. The user may have disabled location services in the Settings application. The device might be in Airplane mode and unable to power up the necessary hardware.
For these reasons, it is recommended that you always call the regionMonitoringAvailable and regionMonitoringEnabled class methods of CLLocationManager before attempting to monitor regions. The regionMonitoringAvailable method lets you know whether the underlying hardware supports region monitoring. If it returns NO, there is no chance that your application will ever be able to use region monitoring on the device. If region monitoring is available, the regionMonitoringEnabled method reports whether
14
the feature is currently enabled. If region monitoring is available but not enabled when you attempt to monitor a region, the system prompts the user to confirm whether region monitoring should be reenabled. Given that the feature is likely to be disabled on purpose, the user might not welcome this prompt.
- (BOOL)registerRegionWithCircularOverlay:(MyCircle*)overlay andIdentifier:(NSString*)identifier { // Do not create regions if support is unavailable or disabled. if ( ![CLLocationManager regionMonitoringAvailable] || ![CLLocationManager regionMonitoringEnabled] ) return NO;
// If the radius is too large, registration fails automatically, // so clamp the radius to the max value. CLLocationDegrees radius = overlay.radius; if (radius > self.locManager.maximumRegionMonitoringDistance) radius = self.locManager.maximumRegionMonitoringDistance;
// Create the region and start monitoring it. CLRegion* region = [[CLRegion alloc] initCircularRegionWithCenter:overlay.coordinate radius:radius identifier:identifier]; [self.locManager startMonitoringForRegion:region
15
desiredAccuracy:kCLLocationAccuracyHundredMeters];
Monitoring of a region begins immediately after registration. However, do not expect to receive an event right away. Only boundary crossings can generate an event. Thus, if at registration time the users location is already inside the region, the location manager does not generate an event. Instead, you must wait for the user to cross the region boundary before an event is generated and sent to the delegate. You should always be judicious when specifying the set of regions to monitor. Regions are a shared system resource and the total number of regions available systemwide is limited. For this reason, Core Location limits the number of regions that may be simultaneously monitored by a single application. To work around these limits, you should consider registering only those regions in the users immediate vicinity. As the users location changes, you can remove regions that are now farther way and add regions coming up on the users path. If you attempt to register a region and space is unavailable, the location manager calls the locationManager:monitoringDidFailForRegion:withError: method of its delegate with the kCLErrorRegionMonitoringFailure error code.
locationManager:didEnterRegion: locationManager:didExitRegion:
The system does not report boundary crossings until the boundary plus a designated cushion distance is exceeded. You specify the desired cushion distance for a region when you register it using the startMonitoringForRegion:desiredAccuracy: method. This cushion value prevents the system from generating numerous entered and exited events in quick succession while the user is traveling close the edge of the boundary. When a region boundary is crossed, the most likely response is to alert the user of the proximity to the target item. If your application is running in the background, you can use local notifications to alert the user; otherwise, you can simply post an alert.
16
Turn off location services when you are not using them. This may seem obvious but it is worth repeating. With the exception of navigation applications that offer turn-by-turn directions, most applications do not need location services to be on all the time. Turn location services on just long enough to get a location fix and then turn them off. Unless the user is in a moving vehicle, the current location should not change frequently enough to be an issue. And you can always start location services again later if needed. Use the significant-change location service instead of the standard location service whenever possible. The significant-change location service provides significant power savings while still allowing you to leave location services running. This is highly recommended for applications that need to track changes in the users location but do not need the higher precision offered by the standard location services. Use lower-resolution values for the desired accuracy unless doing so would impair your application. Requesting a higher accuracy than you need causes Core Location to power up additional hardware and waste power for precision you are not using. Unless your application really needs to know the users position within a few meters, do not put the values kCLLocationAccuracyBest or kCLLocationAccuracyNearestTenMeters in the desiredAccuracy property. And remember that specifying a value of kCLLocationAccuracyThreeKilometers does not prevent the location service from returning better data. Most of the time, Core Location can return location data with an accuracy within a hundred meters or so using Wi-FI and cellular signals.
17
Turn off location events if the accuracy does not improve over a period of time. If your application is not receiving events with the desired level of accuracy, you should look at the accuracy of events you do receive and see if it is improving or staying about the same over time. If accuracy is not improving, it could be because the desired accuracy is simply not available at the moment. Turning off location services and trying again later prevents your application from wasting power.
18
Devices with a magnetometer can report the direction in which a device is pointing, also known as its heading. Devices with GPS hardware can report the direction in which a device is moving, also known as its course.
Remember that heading and course information do not represent the same information. The heading of a device reflects the actual orientation of the device relative to true north or magnetic north. The course of the device represents the direction of travel and does not take into account the device orientation. Depending on your application, you might prefer one over the other or use a combination of the two. For example, a navigation application might toggle between course and heading information depending on the users current speed. At walking speeds, heading information would be more useful for orienting the user to the current environment, whereas in a car, course information provides the general direction of the cars movement.
magnetometerInclude this string if your application requires the presence of heading information. gpsInclude this string if your application requires the presence of course-related information.
Important If your application uses heading or course events but is able to operate successfully without them, do not include the corresponding string value with the UIRequiredDeviceCapabilities key. In both cases, you should also include the location-services string in the array. For more information about the UIRequiredDeviceCapabilities key, see Information Property List Key Reference .
19
Create a CLLocationManager object. Determine whether heading events are available by calling the headingAvailable class method. (In iOS 3.x and earlier, check the value of the headingAvailable property instead.) Assign a delegate to the location manager object. If you want true north values, start location services. Call the startUpdatingHeading method to begin the delivery of heading events.
3. 4. 5.
Listing 2-1 shows a custom method that configures a location manager and starts the delivery of heading events. In this case, the object is a view controller that displays the current heading to the user. Because the view controller displays the true north heading value, it starts location updates in addition to heading updates. This code runs in iOS 4.0 and later
Listing 2-1 Initiating the delivery of heading events
20
// Start location services to get the true heading. locManager.distanceFilter = 1000; locManager.desiredAccuracy = kCLLocationAccuracyKilometer; [locManager startUpdatingLocation];
The object you assign to the delegate property must conform to the CLLocationManagerDelegate protocol. When a new heading event arrives, the location manager object calls the locationManager:didUpdateHeading: method to deliver that event to your application. Upon receiving a new event, you should check the headingAccuracy property to ensure that the data you just received is valid, as shown in Listing 2-2. In addition, if you are using the true heading value, you should also check to see if it contains a valid value before using it.
Listing 2-2 Processing heading events
newHeading.trueHeading : newHeading.magneticHeading);
21
Getting Direction-Related Events Getting Course Information While the User Is Moving
22
Location data is usually returned as a pair of numerical values representing the latitude and longitude of the corresponding point on the globe. These coordinates offer a precise and easy way to specify location data in your code but they are not very intuitive for users. Instead of global coordinates, users are more likely to understand a location that is specified using information they are more familiar with such as street, city, state, and country information. For situations where you want to display a user friendly version of a location, you can use a geocoder object to obtain that information.
Send at most one geocoding request for any one user action. If the user performs multiple actions that involve geocoding the same location, reuse the results from the initial geocoding request instead of starting individual requests for each action. When you want to update the location automatically (such as when the user is moving), reissue the geocoding request only when the user's location has moved a significant distance and after a reasonable amount of time has passed. For example, in a typical situation, you should not send more than one geocoding request per minute. Do not start a geocoding request at a time when the user will not see the results immediately. For example, do not start a request if your application is in the background or was interrupted and is currently in the inactive state.
23
In iOS, you can use either the CLGeocoder or MKReverseGeocoder class to handle reverse-geocoding requests. The CLGeocoder is the preferred class to use and is available in iOS 5.0 and later. However, if your application must run on earlier versions of iOS, you can use the MKReverseGeocoder class.
Converting Coordinates Into Place Name Information shows an example of how to reverse geocode a point on the map. The only code specific to the geocoding request are the first few lines, which allocate the geocoder object as needed and call the reverseGeocodeLocation:completionHandler: method to start the reverse-geocoding operation. (The geocoder variable represents a member variable used to store the geocoder object.) The rest of the code is specific to the sample application itself. In this case, the sample application stores the placemark with a custom annotation object (defined by the MapLocation class) and adds a button to the callout of the corresponding annotation view.
Listing 3-1 Geocoding a location using CLGeocoder
@implementation MyGeocoderViewController (CustomGeocodingAdditions) - (void)geocodeLocation:(CLLocation*)location forAnnotation:(MapLocation*)annotation { if (!geocoder) geocoder = [[CLGeocoder alloc] init];
24
^(NSArray* placemarks, NSError* error){ if ([placemarks count] > 0) { annotation.placemark = [placemarks objectAtIndex:0];
// Add a More Info button to the annotation's view. MKPinAnnotationView* viewForAnnotation:annotation]; view = (MKPinAnnotationView*)[map
if (view && (view.rightCalloutAccessoryView == nil)) { view.canShowCallout = YES; view.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure]; } } }]; } @end
The advantage of using a block object in a sample like this is that information (such as the annotation object) can be easily captured and used as part of the completion handler. Without blocks, the process of wrangling data variables becomes much more complicated.
25
Listing 3-2 shows the code required to use a reverse geocoder. Upon successful completion of the geocoding operation, the code adds a button to the annotation views callout so that it can display the placemark information. Because the annotation is not automatically available to the delegate, the custom annotationForCoordinate: method is included to find the appropriate annotation object from the map view.
Listing 3-2 Geocoding a location using MKReverseGeocoder
@implementation MyGeocoderViewController (CustomGeocodingAdditions) - (void)geocodeLocation:(CLLocation*)location forAnnotation:(MapLocation*)annotation { MKReverseGeocoder* theGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:location.coordinate];
if (!theAnnotation) return;
// Add a More Info button to the annotation's view. MKPinAnnotationView* view = (MKPinAnnotationView*)[map viewForAnnotation:annotation]; if (view && (view.rightCalloutAccessoryView == nil)) { view.canShowCallout = YES; view.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
26
} }
- (void)reverseGeocoder:(MKReverseGeocoder*)geocoder didFailWithError:(NSError*)error { NSLog(@"Could not retrieve the specified place information.\n"); } @end
- (MapLocation*)annotationForCoordinate:(CLLocationCoordinate2D)coord { // Iterate through the map view's list of coordinates // and return the first one whose coordinate matches // the specified value exactly. id<MKAnnotation> theObj = nil;
for (id obj in [self annotations]) { if (([obj isKindOfClass:[MapLocation class]])) { MapLocation* anObj = (MapLocation*)obj;
return theObj; }
27
@end
"Apple Inc "1 Infinite Loop "1 Infinite Loop, Cupertino, CA USA
The more information you can provide to the forward geocoder, the better the results returned to you. The geocoder object parses the information you give it and, if it finds a match, returns some number of placemark objects. The number of returned placemark objects depends greatly on the specificity of the information provided. Thus, providing street, city, province, and country information is much more likely to return a single address than just street and city information. The completion handler block you pass to the geocoder should therefore be prepared to handle multiple placemarks, as shown in the following example:
[geocoder geocodeAddressString:@"1 Infinite Loop" completionHandler:^(NSArray* placemarks, NSError* error){ for (CLPlacemark* aPlacemark in placemarks) { // Process the placemark. } }];
28
Displaying Maps
Introduced in iOS 3.0, the Map Kit framework lets you embed a fully functional map interface into your application. The map support provided by this framework includes many of the features normally found in the Maps application. You can display standard street-level map information, satellite imagery, or a combination of the two. You can zoom and pan the map programmatically, and the framework provides automatic support for the touch events that let users zoom and pan the map. You can also annotate the map with custom information. To use the features of the Map Kit framework, you must link your application to MapKit.framework in your Xcode project. To access the classes and headers of the framework, include an #import <MapKit/MapKit.h> statement at the top of any relevant source files. For general information about the classes of the Map Kit framework, see Map Kit Framework Reference . Important The Map Kit framework uses Google services to provide map data. Use of the framework and its associated interfaces binds you to the Google Maps/Google Earth API terms of service. You can find these terms of service at http://code.google.com/apis/maps/iphone/terms.html.
29
Map Kit uses a Mercator map projection, which is a specific type of cylindrical map projection like the one shown in Figure 4-1 (page 30). In a cylindrical map projection, the coordinates of a sphere are mapped onto the surface of a cylinder, which is then unwrapped to generate a flat map. In such a projection, the longitude lines that normally converge at the poles become parallel instead, causing land masses to be distorted as you move away from the equator. The advantage of a Mercator projection is that the map content is scaled in a way that benefits general navigation. Specifically, on a Mercator map projection, a straight line drawn between any two points on the map yields a course heading that can be used in actual navigation on the surface of the Earth. The projection used by Map Kit uses the Prime Meridian as its central meridian. How you specify data points on a map depends on how you intend to use them. Map Kit supports three basic coordinate systems for specifying map data points:
30
A map coordinate is a latitude and longitude on the spherical representation of the Earth. Map coordinates are the primary way of specifying locations on the globe. You specify individual map coordinate values using the CLLocationCoordinate2D structure. You can specify areas using the MKCoordinateSpan and MKCoordinateRegion structures. A map point is an x and y value on the Mercator map projection. Map points are used for many map-related calculations instead of map coordinates because they simplify the mathematics involved in the calculations. In your application, you use map points primarily when specifying the shape and position of custom map overlays. You specify individual map points using the MKMapPoint structure. You can specify areas using the MKMapSize and MKMapRect structures. A point is a graphical unit associated with the coordinate system of a UIView object. Map points and map coordinates must be mapped to points before drawing custom content in a view. You specify individual points using the CGPoint structure. You can specify areas using the CGSize and CGRect structures.
In most situations, the coordinate system you should use is predetermined by the Map Kit interfaces you are using. When it comes to storing actual data in files or inside your application, map coordinates are precise, portable, and the best option for storing location data. Core Location also uses map coordinates when specifying location values.
Convert from
Map coordinates
Points
Map points
Points
31
Convert from
Convert to
Points
Map coordinates
Points
Map points
To add a map using Interface Builder, drag a Map view object to the appropriate view or window. To add a map programmatically, create an instance of the MKMapView class, initialize it using the initWithFrame: method, and then add it as a subview to your view hierarchy.
Because it is a view, you can manipulate a map view in the same ways you manipulate other views. You can change its size and position in your view hierarchy, you can configure its autoresizing behaviors, and you can add subviews to it. Unlike a view, you never handle touch events directly in a map view. The map view itself is an opaque container for a complex view hierarchy that handles the display of map-related data and all interactions with that data. Any subviews you add to the map view retain the position specified by their frame property and do not scroll with the map contents. If you want content to remain fixed relative to a specific map coordinate (and thus scroll with the map itself ), you must use annotations or overlays as described in Annotating Maps (page 36). New maps are configured to accept user interactions and display map data only. You can configure the map to display satellite imagery or a mixture of satellite and map data by changing the Type attribute of the map in Interface Builder or changing the value in the mapType property. If you want to limit user interactions, you can change the values in the zoomEnabled and scrollEnabled properties as well. If you want to respond to user interactions, you should do so using a delegate as described in Responding to User Interactions with a Map (page 35).
32
The interesting part of an MKCoordinateRegion structure is the span. The span is analogous to the width and height values of a rectangle but is specified using map coordinates and thus is measured in degrees, minutes, and seconds. One degree of latitude is equivalent to approximately 111 kilometers but longitudinal distances vary with the latitude. At the equator, one degree of longitude is equivalent to approximately 111 kilometers but at the poles this value is zero. If you prefer to specify the span using meters, you can use the MKCoordinateRegionMakeWithDistance to create a region data structure using meter values instead of degrees. The value you assign to the region property (or set using the setRegion:animated: method) is usually not the same value that is eventually stored by that property. Setting the span of a region nominally defines the rectangle you want to view but also implicitly sets the zoom level for the map view itself. The map view cannot display arbitrary zoom levels and must adjust any regions you specify to match the zoom levels it supports. It chooses the zoom level that allows your entire region to be visible while still filling as much of the screen as possible. It then adjust the region property accordingly. To find out the resulting region without actually changing the value in the region property, you can use the regionThatFits: method of the map view.
33
To pan the map (but keep the same zoom level), change the value in the centerCoordinate property of the map view or call the setCenterCoordinate:animated: method. To change the zoom level (and optionally pan the map), change the value in the region property of the map view or call the setRegion:animated: method.
If you only want to pan the map, you should only do so by modifying the centerCoordinate property. Attempting to pan the map by changing the region property usually causes a change in the zoom level as well, because changing any part of the region causes the map view to evaluate the zoom level needed to display that region appropriately. Changes to the current latitude almost always cause the zoom level to change and other changes might cause a different zoom level to be chosen as well. Using the centerCoordinate property (or the setCenterCoordinate:animated: method) lets the map view know that it should leave the zoom level unchanged and update the span as needed. For example, to pan the map to the left by half the current map width, you could use the following code to find the coordinate at the left edge of the map and use that as the new center point, as shown here:
CLLocationCoordinate2D mapCenter = myMapView.centerCoordinate; mapCenter = [myMapView convertPoint: CGPointMake(1, (myMapView.frame.size.height/2.0)) toCoordinateFromView:myMapView]; [myMapView setCenterCoordinate:mapCenter animated:YES];
To zoom the map, modify the span of the visible map region. To zoom in, assign a smaller value to the span. To zoom out, assign a larger value. In other words if the current span is one degree, specifying a span of two degrees zooms out by a factor of two:
MKCoordinateRegion theRegion = myMapView.region;
34
The addition of the MKUserLocation annotation object to the map is reported by the delegate in the same way that custom annotations are. If you want to associate a custom annotation view with the users location, you should return that view from your delegate objects mapView:viewForAnnotation: method. If you want to use the default annotation view, you should return nil from that method instead.
Changes to the visible region of the map The loading of map tiles from the network Changes in the users location Changes associated with annotations and overlays.
For information about handling changes associated with annotations and overlays, see Annotating Maps (page 36).
35
Annotating Maps
The MKMapView class implements an opaque view hierarchy for displaying a scrollable map. Although the map itself is scrollable, any subviews you add to a map view remain fixed in place and do not scroll. If you want to affix content to the map itself, and thus have that content scroll along with the rest of the map, you must use annotations and overlays. Annotations are used to display content that can be defined by a single coordinate point. By contrast, overlays are used to display content that is defined by any number of points and may constitute one or more contiguous or noncontiguous shapes. For example, you use annotations to represent information such as the users current location, a specific address, or a single point of interest. You use overlays to present more complex information such as traffic information, the boundaries of parks, lakes, cities, states, countries, or other bounded areas. Map Kit separates the data associated with an annotation or overlay from its visual presentation on the map. This separation allows the map to manage visible annotations and overlays much more efficiently and means that you can add hundreds of annotations and overlays to a map and still expect reasonable performance.
36
In order to display an annotation on a map, your application must provide two distinct objects:
An object that conforms to the MKAnnotation protocol and manages the data for the annotation. (This object is the annotation object.) A view (derived from the MKAnnotationView class) used to draw the visual representation of the annotation on the map surface. (This is the annotation view.)
Annotation objects are typically small data objects that store the map coordinate data and any other relevant information about the annotation, such as a title string. Because annotations are defined using a protocol, you can turn any class in your application into an annotation object. In practice, it is good to keep annotation objects lightweight, especially if you intend to add large numbers of them to the map. The map view keeps a reference to the annotation objects you add to it and uses the data in those objects to determine when to display the corresponding view. Map Kit provides some standard annotation views and you can also define custom annotation views if you want. However, you do not add annotation views directly to the map surface. Instead, you provide an annotation view when asked for it and let the map view incorporate that view into its opaque view hierarchy. You provide the annotation view using your map view delegate object.
37
The annotations you create are typically anchored to a single map coordinate that does not change. However, you can change the coordinate for an annotation programmatically as needed and can implement support to allow the user to drag annotations around the map. In iOS 4.0 and later, support for dragging annotations is even incorporated into the Map Kit classes; implementing this support in prior versions of the operating system requires some additional custom code on your part.
Use the MKPointAnnotation class to implement a simple annotation. This type of annotation contains properties for specifying the title and subtitle strings to display in the annotations onscreen callout bubble. Define a custom object that conforms to the MKAnnotation protocol, as described in Defining a Custom Annotation Object (page 39). This type of annotation can store any type of data you want.
2.
Define an annotation view to present the data on screen. How you define your annotation view depends on your needs and may be one of the following:
If the annotation can be represented by a static image, create an instance of the MKAnnotationView class and assign the image to its image property; see Using the Standard Annotation Views (page 40). If you want to use a standard pin annotation, create an instance of the MKPinAnnotationView class; see Using the Standard Annotation Views (page 40). If a static image is insufficient for representing your annotation, subclass MKAnnotationView and implement the custom drawing code needed to present it. For information about how to implement custom annotation views, see Defining a Custom Annotation View (page 41).
3.
Implement the mapView:viewForAnnotation: method in your map view delegate. Your implementation of this method should dequeue an existing annotation view if one exists or create a new one. If your application supports multiple types of annotations, you must include logic in this method to create a view of the appropriate type for the provided annotation object. For more information about implementing this method, see Creating Annotation Views from Your Delegate Object (page 43).
4.
Add your annotation object to the map view using the addAnnotation: or addAnnotations: method.
38
When you add an annotation to a map view, the map view displays the corresponding annotation view whenever the coordinate for the annotation is in the visible map rectangle. If you want to hide annotations selectively, you must manually remove them from the map view yourself. You can add and remove annotations at any time. All annotations are drawn at the same scale every time, regardless of the maps current zoom level. If your map contains many annotations, this could result in your annotation views overlapping each other as the user zooms out. To counter this behavior, you can add and remove annotations based on the maps current zoom level. For example, a weather application might display information only for major cities when the map is zoomed out to show the entire state. As the user zooms in, the application could then add new annotations containing weather information for smaller cities and regions. Implementing the logic necessary to add and remove annotations is your responsibility. For more information about how to manage the annotations of a map view effectively, see Managing the Maps Annotation Objects (page 45).
@interface MyCustomAnnotation : NSObject <MKAnnotation> { CLLocationCoordinate2D coordinate; } @property (nonatomic, readonly) CLLocationCoordinate2D coordinate; - (id)initWithLocation:(CLLocationCoordinate2D)coord;
39
The implementation for your custom class must provide an implementation for the coordinate property and a way to set its value. Because coordinate is a declared property, you can synthesize the code needed to implement it easily enough using the @synthesize keyword. All that remains is to implement the code for the custom initWithLocation: method, which is shown in Listing 5-2.
Listing 5-2 Implementing the MyCustomAnnotation class
- (id)initWithLocation:(CLLocationCoordinate2D)coord { self = [super init]; if (self) { coordinate = coord; } return self; } @end
Important When you implement the coordinate property in your class, it is recommended that you synthesize its creation. If you choose to implement the methods for this property yourself, or if you manually modify the variable underlying that property in other parts of your class after the annotation has been added to the map, be sure to send out notifications when you do. Map Kit uses KVO notifications to detect changes to the coordinate, title, and subtitle properties of your annotations and make any needed changes to the map display. If you do not send out KVO notifications, the position of your annotations may not be updated properly on the map. For more information about how to implement KVO-compliant accessor methods, see Key-Value Observing Programming Guide . For an example of an annotation object that is based on a Core Data object, see the sample code project WeatherMap .
40
The MKAnnotationView class is perfect for situations where you have a static image that you want to display for an annotation. After creating an instance of this class, assign your custom image to the image property of the object. When the annotation is displayed, the image is displayed centered over the target map coordinate. If you do not want the image to be centered on the map coordinate, you can use the centerOffset property to move the center point horizontally and vertically in any direction. Listing 5-3 shows an example of how to create an annotation view with a custom image and offset.
Listing 5-3 Creating a standard annotation view
MKAnnotationView* aView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"MyCustomAnnotation"] autorelease]; aView.image = [UIImage imageNamed:@"myimage.png"]; aView.centerOffset = CGPointMake(10, -20);
You create the standard annotation views in your delegates mapView:viewForAnnotation: method. For more information about how to implement this method, see Creating Annotation Views from Your Delegate Object (page 43).
41
When drawing content using the drawRect: method, you must always remember to specify a nonzero frame size for your annotation view shortly after initialization. The default initialization method for annotation views does not take a frame rectangle as a parameter. Instead, it uses the image you specify in the image property to set that frame size later. If you do not set an image, though, you must set the frame property of the view explicitly in order for your rendered content to be visible, as shown in Listing 5-5. Because the view draws in only part of its frame, it also sets its opaque property to NO so that the remaining map content shows through. If you do not do this, the drawing system fills your view with the current background color before calling your drawRect: method.
Listing 5-5 Initializing a custom annotation view
- (id)initWithAnnotation:(id <MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]; if (self) { // Set the frame size to the appropriate values. CGRect myFrame = self.frame;
// The opaque property is YES by default. Setting it to // NO allows map content to show through any unrendered // parts of your view. self.opaque = NO; } return self; }
42
In all other respects, drawing custom content in an annotation view is the same as it is in any view. The system calls your views drawRect: method as needed to redraw portions of the view that need it and you can force a redraw operation by calling the setNeedsDisplay or setNeedsDisplayInRect: method of your view at any time. If you want to animate the contents of your view, you need to set up a timer to fire at periodic intervals and update your view. For information on how to set up timers, see Timer Programming Topics . For information about how views draw content in iOS, see View Programming Guide for iOS .
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation { // If it's the user location, just return nil. if ([annotation isKindOfClass:[MKUserLocation class]]) return nil;
43
// Handle any custom annotations. if ([annotation isKindOfClass:[MyCustomAnnotation class]]) { // Try to dequeue an existing pin view first. MKPinAnnotationView* pinView = (MKPinAnnotationView*)[mapView
dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotationView"];
if (!pinView) { // If an existing pin view was not available, create one. pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CustomPinAnnotation"] autorelease]; pinView.pinColor = MKPinAnnotationColorRed; pinView.animatesDrop = YES; pinView.canShowCallout = YES;
// Add a detail disclosure button to the callout. UIButton* rightButton = [UIButton buttonWithType: UIButtonTypeDetailDisclosure]; [rightButton addTarget:self action:@selector(myShowDetailsMethod:) forControlEvents:UIControlEventTouchUpInside]; pinView.rightCalloutAccessoryView = rightButton; } else pinView.annotation = annotation;
return pinView; }
return nil; }
44
In your annotation objects, implement the setCoordinate: method to allow the map view to update the annotations coordinate point. When creating your annotation view, set its draggable property to YES.
When the user touches and holds a draggable annotation view, the map view begins a drag operation for it. As the drag operation progresses, the map view calls the mapView:annotationView:didChangeDragState:fromOldState: method of its delegate to notify it of changes to the drag state of your view. You can use this method to affect or respond to the drag operation. If you want to animate your view during a drag operation, you can do that by implementing a custom dragState method in your annotation view. As the map view processes drag-related touch events, it updates the dragState property of the affected annotation view. Implementing a custom dragState method gives
45
you a chance to intercept these changes and perform additional actions, such as animate the appearance of your view. For example, the MKPinAnnotationView class raises the pin off the map when a drag operation starts and drops the pin back down on the map when it ends. If you need to support draggable annotations in earlier versions of iOS, you must implement the support for it yourself. For information and sample code showing how to do it, see Legacy Map Techniques (page 56).
46
can then be filled or stroked with color. For example, you might use overlays to layer traffic information on top of roadways, highlight the boundaries of a park, or show city, state, and national borders. Figure 5-2 shows a filled and stroked overlay covering the state of Colorado.
Figure 5-2 Displaying an overlay on a map
In order to display an overlay on a map, your application must provide two distinct objects:
An object that conforms to the MKOverlay protocol and manages the data points for the overlay. (This object is the overlay object.) A view (derived from the MKOverlayView class) used to draw the visual representation of the overlay on the map surface. (This is the overlay view.)
47
Overlay objects are typically small data objects that store the points that define the overlay and any other relevant information, such as a title string. Because overlays are defined using a protocol, you can turn any class in your application into an overlay object. In addition, Map Kit defines several concrete overlay objects for specifying different types of standard shapes. The map view keeps a reference to the overlay objects you add to it and uses the data in those objects to determine when to display a corresponding view. Map Kit provides standard overlay views that are capable of drawing any shapes represented by the concrete overlay objects. Like annotations, you do not add overlay views directly to the map surface. Instead, you provide an overlay view when asked for it and let the map view incorporate that view into its opaque view hierarchy. You provide the annotation view using your map view delegate object. Once defined, the position of an overlay on the map typically never changes. Although it is possible to create draggable overlays, doing so is rare and you would need to implement the code to track the dragging operation and update the overlay coordinate points yourself.
Define an appropriate overlay data object using one of the following options:
Use the MKCircle, MKPolygon, or MKPolyline class as-is. Subclass MKShape or MKMultiPoint to create overlays that provide application-specific behaviors or use custom shapes. Use an existing class from your application and make it conform to the MKOverlay protocol.
2.
Define an overlay view to present on the screen using one of the following options:
For standard shapes, use the MKCircleView, MKPolygonView, or MKPolylineView to represent the annotation. You can customize many of the drawing attributes of the final shape using these classes. For custom shapes descended from MKShape, define an appropriate subclass of MKOverlayPathView to render the shape. For all other custom shapes and overlays, subclass MKOverlayView and implement your custom drawing code.
3. 4.
Implement the mapView:viewForOverlay: method in your map view delegate. Add your overlay data object to the map view using the addOverlay: method or one of many others.
48
Unlike annotations, rendered overlays are automatically scaled to match the current zoom level of the map. Scaling the overlay is necessary because overlays generally highlight boundaries, roads, and other content that also scales during zooming. In addition, you can rearrange their Z-ordering of overlays in a map to ensure that specific overlays are always displayed on top of others.
points[0] = CLLocationCoordinate2DMake(41.000512, -109.050116); points[1] = CLLocationCoordinate2DMake(41.002371, -102.052066); points[2] = CLLocationCoordinate2DMake(36.993076, -102.041981); points[3] = CLLocationCoordinate2DMake(36.99892, -109.045267);
[map addOverlay:poly];
In order for an overlay can be shown on the map, the mapView:viewForOverlay: method of your map view delegate needs to provide an appropriate overlay view. For the standard overlay shapes, you can do this by creating a view that matches the type of shape you want to display. Listing 5-8 shows an implementation of this method that creates the polygon view used to cover the state of Colorado. In this example, the method sets the colors to use for rendering the shape and the border width.
49
Listing 5-8
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay { if ([overlay isKindOfClass:[MKPolygon class]]) { MKPolygonView* aView = [[[MKPolygonView alloc] initWithPolygon:(MKPolygon*)overlay] autorelease];
return aView; }
return nil; }
It is important to remember that the standard overlay views are there to simply fill and stroke the shape represented by the overlay. If you want to display additional information, you need to create a custom overlay view to do the necessary drawing. You should avoid adding subviews to an existing overlay in an attempt to render any extra content. Any subviews you add to an overlay are scaled along with the overlay itself and made to fit the zoom level of the map. Unless your subviews contain content that also scales well, the results would probably not look very good.
50
A coordinate defining the center point of the overlay A bounding rectangle that completely encompasses the overlays content
Of the two pieces of information, the bounding rectangle is the one that is most important to the overlay itself. The map view uses the bounding rectangle specified by an overlay object as its cue for when to add the corresponding overlay view to the map. (If you add the overlay to the map as an annotation as well, the coordinate value similarly determines when the corresponding annotation view should be added to the map.) The bounding rectangle itself must be specified using map points, not map coordinates. You can convert between the two coordinate systems using the Map Kit functions. Most of the real work involved with displaying an overlay is incurred by the corresponding overlay view object. The overlay object simply defines where on the map the overlay should be placed, whereas the overlay view defines the final appearance of the overlay, including what information (if any) is displayed for the overlay. The creation of custom overlay views is described further in Defining a Custom Overlay View (page 51).
drawMapRect:zoomScale:inContext: to draw your custom content canDrawMapRect:zoomScale: if your drawing code depends on content that might not always be
available The canDrawMapRect:zoomScale: method is for situations where your content may not always be ready to draw. For example, a traffic overlay would need to download the needed traffic data from the network before it could draw. If you return NO from this method, the map view refrains from drawing your view until you signal that you are ready. You can do this by marking your view as dirty using either the setNeedsDisplayInMapRect: or setNeedsDisplayInMapRect:zoomScale: method. When your view is ready to draw, the map view calls the drawMapRect:zoomScale:inContext: method to do the actual drawing. Unlike drawing in a normal view, drawing in an overlay view involves some special considerations, including the following:
51
Your drawing code should never use the views bounds or frame as reference points for drawing. Instead, it should use the map points associated with the overlay object to define shapes. Immediately before drawing, it should then convert those map points to points (CGPoint and so on) using the conversion routines found in the MKOverlayView class. Also, you typically do not apply the zoom scale value passed to this method directly to your content. Instead, you provide it only when a Map Kit function or method specifically requires it. As long as you specify content using map points and convert to points, your content should be scaled to the correct size automatically.
If you use UIKit classes and functions to draw, you must explicitly set up and clean up the drawing environment. Before issuing any calls, call the UIGraphicsPushContext function to make the context passed to your method the current context. When you are done drawing, call UIGraphicsPopContext to remove that context. Remember that the map view may tile large overlays and render each tile on a separate thread. Your drawing code should therefore not attempt to modify variables or other data unless it can do so in a thread-safe manner.
Listing 5-9 shows the drawing code used to fill the bounding rectangle of an overlay using a gradient. When drawing gradients, it is especially important to contain the drawing operation by applying a clipping rectangle to the desired drawing area. The views frame is actually larger than the overlays bounding rectangle, so without a clipping rectangle, the gradient would render outside the expected area. Because the bounding rectangle of the overlay defines the actual shape in this case, this method simply clips to the bounding rectangle. For more complex overlays, you would want to clip to the path representing your overlay. The results of this drawing code are shown in Figure 5-3 (page 54).
Listing 5-9 Drawing a gradient in a custom overlay view
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context { // Get the overlay bounding rectangle. MKMapRect theMapRect = [self.overlay boundingMapRect];
52
// Set up the gradient color and location information. CGColorSpaceRef myColorSpace = CGColorSpaceCreateDeviceRGB(); CGFloat locations[4] = {0.0, 0.33, 0.66, 1.0}; CGFloat components[16] = {0.0, 0.0, 1.0, 0.5, 1.0, 1.0, 1.0, 0.8, 1.0, 1.0, 1.0, 0.8, 0.0, 0.0, 1.0, 0.5};
// Create the gradient. CGGradientRef myGradient = CGGradientCreateWithColorComponents(myColorSpace, components, locations, 4); CGPoint start, end; start = CGPointMake(CGRectGetMidX(theRect), CGRectGetMinY(theRect)); end = CGPointMake(CGRectGetMidX(theRect), CGRectGetMaxY(theRect));
53
Figure 5-3 shows the results of drawing custom content over the overlay for the state of Colorado. In this case, the overlay view fills its content with a custom gradient.
Figure 5-3 Using a custom overlay view to draw
54
to the one shown in Listing 5-8 (page 50). However, if each overlay uses different colors or drawing attributes, you should find a way to initialize that information using the annotation object, rather than having a large decision tree in this method. Because overlays are typically different from one another, the map view does not recycle those views when they are removed from the map. Instead of dequeueing an existing overlay view, you must create a new overlay view every time.
55
The following sequence of code listings shows you how to implement a user-movable annotation view in iOS 3.x. In this example, the annotation view displays a bulls-eye image directly over the annotations coordinate point and includes a custom accessory view for displaying details about the target. Listing A-1 shows the definition of the BullseyeAnnotationView class. The class includes some additional member variables that it uses during tracking to move the view correctly. It also stores a pointer to the map view itself, the value for which is set by the code in the mapView:viewForAnnotation: method when it creates or reinitializes the annotation view. The map view object is needed when event tracking is finished to adjust the map coordinate of the annotation object.
Listing A-1 The BullseyeAnnotationView class
MKMapView* map; }
56
- (id)initWithAnnotation:(id <MKAnnotation>)annotation;
@end
@implementation BullseyeAnnotationView @synthesize map; - (id)initWithAnnotation:(id <MKAnnotation>)annotation { self = [super initWithAnnotation:annotation reuseIdentifier:@"BullseyeAnnotation"]; if (self) { UIImage* theImage = [UIImage imageNamed:@"bullseye32.png"];
UIButton*
57
When a touch event first arrives in a bulls-eye view, the touchesBegan:withEvent: method of that class records information about the initial touch event, as shown in Listing A-2. It uses this information later in its touchesMoved:withEvent: method to adjust the position of the view. All location information is stored in the coordinate space of the superview.
Listing A-2 Tracking the views location
@implementation BullseyeAnnotationView (TouchBeginMethods) - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // The view is configured for single touches only. UITouch* aTouch = [touches anyObject]; startLocation = [aTouch locationInView:[self superview]]; originalCenter = self.center;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* aTouch = [touches anyObject]; CGPoint newLocation = [aTouch locationInView:[self superview]]; CGPoint newCenter;
// If the user's finger moved more than 5 pixels, begin the drag. if ( (abs(newLocation.x - startLocation.x) > 5.0) || (abs(newLocation.y - startLocation.y) > 5.0) ) isMoving = YES;
// If dragging has begun, adjust the position of the view. if (isMoving) { newCenter.x = originalCenter.x + (newLocation.x - startLocation.x); newCenter.y = originalCenter.y + (newLocation.y - startLocation.y); self.center = newCenter; }
58
else
When the user stops dragging an annotation view, you need to adjust the coordinate of the original annotation to ensure the view remains in the new position. Listing A-3 shows the touchesEnded:withEvent: method for the BullseyeAnnotationView class. This method uses the map member variable to convert the pixel-based point into a map coordinate value. Because the coordinate property of an annotation is normally read-only, the annotation object in this case implements a custom changeCoordinate method to update the value it stores locally and reports using the coordinate property. If the touch event was canceled for some reason, the touchesCancelled:withEvent: method returns the annotation view to its original position.
Listing A-3 Handling the final touch events
@implementation BullseyeAnnotationView (TouchEndMethods) - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (isMoving) { // Update the map coordinate to reflect the new position. CGPoint newCenter = self.center; BullseyeAnnotation* theAnnotation = self.annotation; CLLocationCoordinate2D newCoordinate = [map convertPoint:newCenter toCoordinateFromView:self.superview];
[theAnnotation changeCoordinate:newCoordinate];
// Clean up the state information. startLocation = CGPointZero; originalCenter = CGPointZero; isMoving = NO; } else [super touchesEnded:touches withEvent:event]; }
59
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { if (isMoving) { // Move the view back to its starting point. self.center = originalCenter;
// Clean up the state information. startLocation = CGPointZero; originalCenter = CGPointZero; isMoving = NO; } else [super touchesCancelled:touches withEvent:event]; } @end
60
Notes Added information about the new Core Location geocoders. Added information about region monitoring. Added information about creating overlays. Expanded the existing information about maps and annotations. Updated the location-related sections to cover new technologies for obtaining the users location.
2010-03-24
New document describing how to use location and map services in an application.
61
Apple Inc. 2011 Apple Inc. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, mechanical, electronic, photocopying, recording, or otherwise, without prior written permission of Apple Inc., with the following exceptions: Any person is hereby authorized to store documentation on a single computer for personal use only and to print copies of documentation for personal use provided that the documentation contains Apples copyright notice. The Apple logo is a trademark of Apple Inc. No licenses, express or implied, are granted with respect to any of the technology described in this document. Apple retains all intellectual property rights associated with the technology described in this document. This document is intended to assist application developers to develop applications only for Apple-labeled computers. Apple Inc. 1 Infinite Loop Cupertino, CA 95014 408-996-1010 App Store is a service mark of Apple Inc. Apple, the Apple logo, Objective-C, and Xcode are trademarks of Apple Inc., registered in the United States and other countries. IOS is a trademark or registered trademark of Cisco in the U.S. and other countries and is used under license.
Even though Apple has reviewed this document, APPLE MAKES NO WARRANTY OR REPRESENTATION, EITHER EXPRESS OR IMPLIED, WITH RESPECT TO THIS DOCUMENT, ITS QUALITY, ACCURACY, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. AS A RESULT, THIS DOCUMENT IS PROVIDED AS IS, AND YOU, THE READER, ARE ASSUMING THE ENTIRE RISK AS TO ITS QUALITY AND ACCURACY. IN NO EVENT WILL APPLE BE LIABLE FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES RESULTING FROM ANY DEFECT OR INACCURACY IN THIS DOCUMENT, even if advised of the possibility of such damages. THE WARRANTY AND REMEDIES SET FORTH ABOVE ARE EXCLUSIVE AND IN LIEU OF ALL OTHERS, ORAL OR WRITTEN, EXPRESS OR IMPLIED. No Apple dealer, agent, or employee is authorized to make any modification, extension, or addition to this warranty. Some states do not allow the exclusion or limitation of implied warranties or liability for incidental or consequential damages, so the above limitation or exclusion may not apply to you. This warranty gives you specific legal rights, and you may also have other rights which vary from state to state.