If there is one fact about Apple that we can state with any degree of certainty, it is that the company is passionate about retaining control of its destiny. Unfortunately, one glaring omission in this overriding corporate strategy has been the reliance on a competitor (in the form of Google) for mapping data in iOS. This dependency officially ended with iOS 6 through the introduction of Apple Maps.
In iOS 8, Apple Maps officially replaced the Google-based map data with data provided primarily by TomTom (but also technology from other companies, including some acquired by Apple for this purpose). Headquartered in the Netherlands, TomTom specializes in mapping and GPS systems. Of particular significance, however, is that TomTom (unlike Google) does not make smartphones, nor does it develop an operating system that competes with iOS, making it a more acceptable partner for Apple.
As part of the iOS 6 revamp of mapping, the SDK also introduced a class called MKMapItem, designed solely to ease the integration of maps and turn-by-turn directions into iOS apps. This was further enhanced in iOS 9 with the introduction of support for transit times, directions, and city flyover support.
For more advanced mapping requirements, the iOS SDK also includes the original classes of the MapKit framework, details of which will be covered in later chapters.
MKMapItem and MKPlacemark Classes
The MKMapItem class aims to make it easy for apps to launch maps without writing significant amounts of code. MKMapItem works in conjunction with the MKPlacemark class, instances of which are passed to MKMapItem to define the locations that are to be displayed in the resulting map. A range of options is also provided with MKMapItem to configure both the appearance of maps and the nature of directions to be displayed (i.e., whether directions are for driving, walking, or public transit).
An Introduction to Forward and Reverse Geocoding
It is difficult to talk about mapping, particularly when dealing with the MKPlacemark class, without first venturing into geocoding. Geocoding can best be described as converting a textual-based geographical location (such as a street address) into geographical coordinates expressed in longitude and latitude.
In iOS development, geocoding may be performed using the CLGeocoder class to convert a text-based address string into a CLLocation object containing the coordinates corresponding to the address. The following code, for example, converts the street address of the Empire State Building in New York to longitude and latitude coordinates:
let addressString = "350 5th Avenue New York, NY"
CLGeocoder().geocodeAddressString(addressString,
completionHandler: {(placemarks, error) in
if error != nil {
print("Geocode failed with error: \(error!.localizedDescription)")
} else if let marks = placemarks, marks.count > 0 {
let placemark = marks[0]
if let location = placemark.location {
let coords = location.coordinate
print(coords.latitude)
print(coords.longitude)
}
}
})
Code language: Swift (swift)
The code calls the geocodeAddressString method of a CLGeocoder instance, passing through a string object containing the street address and a completion handler to be called when the translation is complete. Passed as arguments to the handler are an array of CLPlacemark objects (one for each match for the address) together with an Error object which may be used to identify the reason for any failures.
For this example, the assumption is made that only one location matched the address string provided. The location information is then extracted from the CLPlacemark object at location 0 in the array, and the coordinates are displayed on the console.
The above code is an example of forward geocoding in that coordinates are calculated based on a text address description. Reverse geocoding, as the name suggests, involves the translation of geographical coordinates into a human-readable address string. Consider, for example, the following code:
let newLocation = CLLocation(latitude: 40.74835, longitude: -73.984911)
CLGeocoder().reverseGeocodeLocation(newLocation, completionHandler: {(placemarks, error) in
if error != nil {
print("Geocode failed with error: \(error!.localizedDescription)")
}
if let marks = placemarks, marks.count > 0 {
let placemark = marks[0]
let postalAddress = placemark.postalAddress
if let address = postalAddress?.street,
let city = postalAddress?.city,
let state = postalAddress?.state,
let zip = postalAddress?.postalCode {
print("\(address) \(city) \(state) \(zip)")
}
}
})
Code language: Swift (swift)
In this case, a CLLocation object is initialized with longitude and latitude coordinates and then passed through to the reverseGeocodeLocation method of a CLGeocoder object. Next, the method passes through an array of matching addresses to the completion handler in the form of CLPlacemark objects. Each placemark contains Integrating Maps into iOS 16 Apps using MKMapItem
the address information for the matching location in the form of a CNPostalAddress object. Once again, the code assumes a single match is contained in the array and accesses and displays the address, city, state, and zip properties of the postal address object on the console.
When executed, the above code results in output that reads:
338 5th Ave New York New York 10001
Code language: plaintext (plaintext)
It should be noted that the geocoding is not performed on the iOS device but rather on a server to which the device connects when a translation is required, and the results are subsequently returned when the translation is complete. As such, geocoding can only occur when the device has an active internet connection.
Creating MKPlacemark Instances
Each location to be represented when a map is displayed using the MKMapItem class must be represented by an MKPlacemark object. When MKPlacemark objects are created, they must be initialized with the geographical coordinates of the location together with an NSDictionary object containing the address property information. Continuing the example of the Empire State Building in New York, an MKPlacemark object would be created as follows:
import Contacts
import MapKit
.
.
let coords = CLLocationCoordinate2DMake(40.7483, -73.984911)
let address = [CNPostalAddressStreetKey: "350 5th Avenue",
CNPostalAddressCityKey: "New York",
CNPostalAddressStateKey: "NY",
CNPostalAddressPostalCodeKey: "10118",
CNPostalAddressISOCountryCodeKey: "US"]
let place = MKPlacemark(coordinate: coords, addressDictionary: address)
Code language: Swift (swift)
While it is possible to initialize an MKPlacemark object passing through a nil value for the address dictionary, this will result in the map appearing, albeit with the correct location marked, but it will be tagged as “Unknown” instead of listing the address. The coordinates are, however, mandatory when creating an MKPlacemark object. If the app knows the text address but not the location coordinates, geocoding will need to be used to obtain the coordinates before creating the MKPlacemark instance.
Working with MKMapItem
Given the tasks it can perform, the MKMapItem class is extremely simple to use. In its simplest form, it can be initialized by passing through a single MKPlacemark object as an argument, for example:
let mapItem = MKMapItem(placemark: place)
Code language: Swift (swift)
Once initialized, the openInMaps(launchOptions:) method will open the map positioned at the designated location with an appropriate marker, as illustrated in Figure 64-1:
mapItem.openInMaps(launchOptions: nil)
Code language: Swift (swift)
Similarly, the map may be initialized to display the current location of the user’s device via a call to the MKMapItem forCurrentLocation method:
let mapItem = MKMapItem.forCurrentLocation()
Code language: Swift (swift)
Multiple locations may be tagged on the map by placing two or more MKMapItem objects in an array and then passing that array through to the openMaps(with:) class method of the MKMapItem class. For example:
let mapItems = [mapItem1, mapItem2, mapItem3]
MKMapItem.openMaps(with: mapItems, launchOptions: nil)
Code language: Swift (swift)
MKMapItem Options and Configuring Directions
In the example code fragments presented in the preceding sections, a nil value was passed through as the options argument to the MKMapItem methods. In fact, several configuration options are available for use when opening a map. These values need to be set up within an NSDictionary object using a set of pre-defined keys and values:
- MKLaunchOptionsDirectionsModeKey – Controls whether directions are to be provided with the map. If only one placemarker is present, directions from the current location to the placemarker will be provided. The mode for the directions should be either MKLaunchOptionsDirectionsModeDriving, MKLaunchOptionsDirectionsModeWalking, or MKLaunchOptionsDirectionsModeTransit.
- MKLaunchOptionsMapTypeKey – Indicates whether the map should display standard, satellite, hybrid, flyover, or hybrid flyover map images.
- MKLaunchOptionsMapCenterKey – Corresponds to a CLLocationCoordinate2D structure value containing the coordinates of the location on which the map is to be centered.
- MKLaunchOptionsMapSpanKey – An MKCoordinateSpan structure value designating the region the map should display when launched.
- MKLaunchOptionsShowsTrafficKey – A Boolean value indicating whether traffic information should be Integrating Maps into iOS 16 Apps using MKMapItem superimposed over the map when it is launched.
- MKLaunchOptionsCameraKey – When displaying a map in 3D flyover mode, the value assigned to this key takes the form of an MKMapCamera object configured to view the map from a specified perspective.
The following code, for example, opens a map with traffic data displayed and includes turn-by-turn driving directions between two map items:
let mapItems = [mapItem1, mapItem2]
let options = [MKLaunchOptionsDirectionsModeKey:
MKLaunchOptionsDirectionsModeDriving,
MKLaunchOptionsShowsTrafficKey: true] as [String : Any]
MKMapItem.openMaps(with: mapItems, launchOptions: options)
Code language: Swift (swift)
Adding Item Details to an MKMapItem
When a location is marked on a map, the address is displayed together with a blue arrow, which displays an information card for that location when selected.
The MKMapItem class allows additional information to be added to a location through the name, phoneNumber, and url properties. The following code, for example, adds these properties to the map item for the Empire State Building:
mapItem.name = "Empire State Building"
mapItem.phoneNumber = "+12127363100"
mapItem.url = URL(string: "https://esbnyc.com")
mapItem.openInMaps(launchOptions: nil)
Code language: Swift (swift)
When the code is executed, the map place marker displays the location name instead of the address, together with the additional information:
A force touch performed on the marker displays a popover panel containing options to call the provided number or visit the website:
Summary
iOS 6 replaced Google Maps with maps provided by TomTom. Unlike Google Maps, which was assembled from static images, the new Apple Maps are dynamically rendered, resulting in clear and smooth zooming and more precise region selections. iOS 6 also introduced the MKMapItem class, which aims to make it easy for iOS app developers to launch maps and provide turn-by-turn directions with the minimum amount of code.
Within this chapter, the basics of geocoding and the MKPlacemark and MKMapItem classes have been covered. The next chapter, entitled An Example iOS 16 MKMapItem App, will work through creating an example app that utilizes the knowledge covered in this chapter.