От халепа... Ця сторінка ще не має українського перекладу, але ми вже над цим працюємо!
От халепа... Ця сторінка ще не має українського перекладу, але ми вже над цим працюємо!
Supernerd
/
Developer
8 min read
Google Maps has rightfully earned its spot as a top navigation solution for users and engineers. And even if you’re into iOS application software development, Google Maps will still be your mapping service of choice. But while there is an official Google Maps SDK for iOS, features like marker clustering can cause a lot of pain (and rage). Especially when you can’t, for the life of you, understand what’s wrong with the code.
The Google Maps library is quite tricky to work with. Written in Objective-C 9,000 years ago (or so), it is still used extensively — but working with it is far from pleasant. In this article, we’ll show you how to prepare a project for using Google Maps, its basic functionality, and how to implement marker clustering using Google Maps iOS Utils. Naturally, we’ll illustrate this with code snippets. BTW, all the code is available in this GitHub repo, so you are free to play with it.
NB: This tutorial is written using Xcode 12.1 and Google Maps pod 4.0.0. You will also need to have a Google account.
Start with creating a new Xcode project called GoogleMapsClustersTest.
1. Delete the default ViewController class.
2. Create a Cocoa Touch Class named MapViewController subclassing UIViewController. Of course, you are free to simply rename the default one.
3. Assign this class to the ViewController in Main.storyboard.
That’s all the work we need to do with the storyboard.
Now, log into your Google account, go tohttps://mapsplatform.google.com/, and click Get started.
You will be directed to a list of various APIs. Look for Maps SDK for iOS.
Just like in Firebase, you have to create your project in this console, so click CREATE PROJECT.
Remember the project name has to be the same as the name of your Xcode project, so, in our case, it’s GoogleMapsClustersTest.
After that, go back to the console and click Enable APIs, select iOS API, and click Enable.
In the side menu on the left, click Credentials — that’s where you have to create your own API Key. Click CREATE CREDENTIALS and API Key.
After the installation is complete, open GoogleMapsClustersTest.xcworkspace and go to AppDelegate. You should import the Google Maps module there.
Now, it’s time to actually import Google Maps SDK into our project. Close Xcode and open Terminal to create a Podfile. For this project, we will require the Google Maps library. So, in the newly created Podfile in use_frameworks! add:
pod ‘GoogleMaps’
Once it’s done, go back to Credentials in the Google console and copy your API key to the clipboard, then add it back in AppDelegate as a string constant. You can name it googleMapsApiKey or in any other way. Inside didFinishLaunchingWithOptions, call GMSServices and its method provideAPIKeyand pass the newly created constant.
let googleMapsApiKey = “Your_API_key” func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { GMSServices.provideAPIKey(googleMapsApiKey) return true }
Our app is now connected to Google server and is almost ready for the first launch. However, we still need to configure a view that will display the map itself.
You might think it’s a good idea to drop UIView on your MapViewController, but later, you will inevitably encounter an error saying this new View is actually nil. That is why you need to create it programmatically. Don’t worry — it’s simple. Just don’t forget to import the Google Maps framework to the MapViewControllerclass. Now, you can create an optional variable called googleMapsViewof the GMSMapView? type.
import GoogleMaps import UIKit class MapViewController: UIViewController { var googleMapsView: GMSMapView?
The map view is ready, but it still needs some tweaking.
Let’s create a function for adjusting our MapView parameters and call it setupMapView.
First, create a camera — think of it as a real camera over a real map. It would be nice to set a specific position and not scroll from Britain (default initial coordinates) to your map pins. For that, you’ll need the simplest initializer with latitude, longitude, and zoom values.
func setupMapView() { let camera = GMSCameraPosition.camera(withLatitude: 50.4566, longitude: 30.5238, zoom: 6.0) googleMapsView = GMSMapView.map(withFrame: CGRect.zero, camera: camera) view = googleMapsView }
Second, create the actual MapView, and state that the map of our googleMapsView will fill the entire screen and use the newly created camera. You may be thinking, why does CGRect.zero work in a way contrary to its meaning? Well, that’s because in the very next line, you have to assign your googleMapsView to your main view of MapViewController, and it is always resized by the parent ViewController to fill the window.
Now, it’s time to put some pins on our map. A good practice is to encapsulate this logic in a separate function — we called it placeMarker. It takes one parameter of CLLocationCoordinate2D, which is a struct with two values we need: latitude and longitude.
func placeMarker(location: CLLocationCoordinate2D) { let marker = GMSMarker(position: CLLocationCoordinate2DMake(location.latitude, location.longitude)) let icon = UIImage(named: "MapPoint")?.withRenderingMode(.alwaysTemplate) let markerView = UIImageView(image: icon) marker.iconView = markerView marker.tracksViewChanges = true marker.map = googleMapsView }
In this function, we created an object of the GMSMarker class and gave it an ImageView with an image from our xcodeAssets named MapPoint. Of course, you are free to use any image you want. After that, we had to specify that this marker would be used on our map (called googleMapsView).
Since there’s no server in this app, let’s use hardcoded values. Thanks to the aforementioned CLLocationCoordinate2D struct, you can create an array of locations in MapViewController. To spare you the time to retype all these coordinates, just copy this array into your project:
let locationsArray = [CLLocationCoordinate2D(latitude: 50.455639, longitude: 30.521833), CLLocationCoordinate2D(latitude: 50.452778, longitude: 30.514444), CLLocationCoordinate2D(latitude: 50.463056, longitude: 30.516389), CLLocationCoordinate2D(latitude: 50.401667, longitude: 30.513611), CLLocationCoordinate2D(latitude: 50.467611, longitude: 30.491944)]
It’s finally time to actually place these markers on our map. In the viewDidLoad method, aftercalling the setupMapView function, add the for-loop that will iterate over the locations in your locationsArray. Now, your viewDidLoad should look like this:
override func viewDidLoad() { super.viewDidLoad() setupMapView() for location in locationsArray { placeMarker(location: location) } }
Hooray – we’ve just placed pins on our map!
But even with the small zoom value, you see that your markers blur together. Now, imagine having not five but five hundred pins. They would cover the entire map and make navigation unbearable. Not to mention the resources needed to render each and every pin. That’s where clusters come in.
A cluster is a separate object in the Google Maps framework. Since you need to implement clusters in your app, the Google Maps pod itself won’t suffice. Here’s what you should do.
Create a Google-Maps-iOS-Utils group in your project directory. Then, go to this repository and download the demo application. You need to import the content of the src group to the Google-Maps-iOS-Utils folder you just created, with the exception of the Heatmap folder.
As you can see, we are dealing with Objective-C here, so we need a Bridging header. Create a header file called BringingHeader and add a single line:
#import “GMUMarkerClustering.h”
Next, go to Targets > Build Settings and type Bridging Header in the search field. In the Swift Compiler – General section, double click the field Objective-C Bridging Header and paste the address of your newly created bridging header: GoogleMapsClustersTest/BridgingHeader.h.
If you try to build the application, Xcode will display two errors, both of them coming from the GMUMarkerClustering.h file.
The solution is pretty easy: replace the triangle brackets with regular ones and remove the Google-Maps-iOS-Utils/ part from all paths, leaving file names only.
Declare a new optional variable called clusterManager of the GMUClusterManager type. Then, create an extension for MapViewController with the conformance to GMUClusterManagerDelegate, GMSMapViewDelegate, and GMUClusterIconGenerator protocols. The only stub we get is an icon function returning the default image for the cluster icon. Let’s use the same image we used for a regular marker.
extension MapViewController: GMUClusterManagerDelegate, GMSMapViewDelegate, GMUClusterIconGenerator { func icon(forSize size: UInt) -> UIImage! { return UIImage(named: "MapPoint") } }
Our ClusterManager needs some configuration as well. Inside the same extension, create a function called setupClusterManager. Since our clusters can have different capacities, we can assign them different icons. But to keep it simple, let’s create imagesArray containing one clusterIconImage.
func setupClusterManager() { // Register self to listen to both GMUClusterManagerDelegate and GMSMapViewDelegate events. // Set up the cluster manager with the supplied icon generator and renderer. let clusterIconImage = UIImage(named: "MapPoint") let imagesArray = [clusterIconImage, clusterIconImage, clusterIconImage, clusterIconImage] let iconGenerator = GMUDefaultClusterIconGenerator(buckets: [5, 10, 20, 100], backgroundImages: imagesArray as! [UIImage]) let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm() // Generate and add random items to the cluster manager. // Call cluster() after items have been added to perform the clustering and rendering on map. guard let mapView = googleMapsView else { return } let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator) clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer) clusterManager?.cluster() }
Then, specify the cluster capacities using the GMUDefaultClusterIconGenerator class. In its initializer, it has a buckets parameter that takes an array of int values. Specifying 20, 100, 200, you won’t see a cluster saying it holds some interjacent amount of markers, like 93 or 168.
The latest call in this function is the cluster method, which actually arranges items into groups and is called on every zoom level change. You’ll need to get back to it later.
Since we’re working with delegates (GMUClusterManagerDelegate and GMSMapViewDelegate), specify that these delegates will send messages to your class, which is MapViewController. This time, set them both in one call, so it would be nice to, guess what – put this declaration in a separate function along with the call of the setupClusterManager function we mentioned above.
func setupClusters() { setupClusterManager() clusterManager?.setDelegate(self, mapDelegate: self) }
The logic for the cluster itself is complete. The only thing left is to tell your clusterManager what markers can actually be grouped into clusters. Write a function called createClusters with one parameter location of type CLLocationCoordinate2D.
func createClusters(location: CLLocationCoordinate2D) { let marker = GMSMarker(position: location) clusterManager?.add(marker) }
As you see, we created a marker using the provided position. Keep in mind, though, clusterManager doesn’t work with GMSMarker objects (oddly enough, it is contrary to Google’s documentation on the subject). The only workaround for this issue is creating a class conforming to the GMUClusterItem protocol. Let’s call it POIItem (abbreviation for Point Of Interest Item) and import the Google Maps framework.
You will instantly get an error about the nonconformance to NSObjectProtocol. As the error itself suggests, add inheritance to NSObject. The required value is a position of the CLLocationCoordinate2D type. You are free to add your own custom fields to this class, such as id, name, and others.
import GoogleMaps class POIItem: NSObject, GMUClusterItem { var position: CLLocationCoordinate2D var marker: GMSMarker? init(position: CLLocationCoordinate2D, marker: GMSMarker) { self.position = position self.marker = marker } }
Let’s go back to our createClusters function and create an object called item of the POIItem type. Call the clusterManager’s function add and pass the newly created item to it.
func createClusters(location: CLLocationCoordinate2D) { let marker = GMSMarker(position: location) let item = POIItem(position: location, marker: marker) clusterManager?.add(item) }
This time, let’s go back to the viewDidLoad method and move for-loop to the bottom. Replace the placeMarker call with createClusters and watch the results. Simply put, instead of grouping the already placed markers into clusters, you initially have to create a cluster holding those markers, where each of them keeps its own location.
override func viewDidLoad() { super.viewDidLoad() setupMapView() setupClusters() for location in locationsArray { createClusters(location: location) } }
Launch the application.
Surprisingly, there is no cluster. While trying to figure out whether it’s frozen or not, you may change the zoom level, and only then will the cluster appear. As we mentioned earlier, the method that actually groups objects into clusters is clusterManager’s function cluster. Let’s call our clusterManager and its cluster method at the end of the createClusters function.
func createClusters(location: CLLocationCoordinate2D) { let marker = GMSMarker(position: location) let item = POIItem(position: location, marker: marker) clusterManager?.add(item) clusterManager?.cluster() }
Launch the application now.
This is the cluster we’ve been looking for. Note how it decomposes into separate markers upon zooming. Zooming back will make the makers group into a cluster again.
Don’t forget the markers still have the default Google Maps icon, and this will be the last issue covered in this guide. Add the GMUClusterRendererDelegate protocol at the end of our extension.
extension MapViewController: GMUClusterManagerDelegate, GMSMapViewDelegate, GMUClusterIconGenerator, GMUClusterRendererDelegate {
Now, you can call the willRenderMarker function. It is called every time a marker is about to be placed on the map. We need to run a check for this function to know if the given marker belongs to a cluster. Previously, we created a custom POItem class to make GMSMarker work with clusterManager. Remember, that we don’t check the marker itself, but its userData property, which, according to the description, is “overlay data. You can use this property to associate an arbitrary object with this overlay.”
func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) { if marker.userData as? POIItem != nil { let icon = UIImage(named: "MapPoint") marker.iconView = UIImageView(image: icon) } }
Launch the application and zoom in. Congrats! Now, the markers are using the same icon as the cluster.
It took us some time to fix bugs and find workarounds while writing this guide on implementing clusters using Google Maps iOS Utilities (one of a kind, we should say). And now, you’re welcome to use it and share it. Trust us, it will spare you hours of reading StackOverflow threads and GitHub issue reports to find answers about Google Maps-enabled iOS app development.
This is just one example of how NerdzLab handles iOS app development in general and map marker clustering in particular. Another one is Search Party, an app we developed, that is among the top 10 social apps in Australia. It uses Mapbox and other utilities to allow more than 100,000 registered users to quickly form parties and locate each other in crowds during festivals.
So, if you need assistance with implementing clustering with Google Maps for iOS apps, native app development, or product design, contact us. We’re ready to help!