Maps are used in many mobile apps. That’s mainly because of the nature of the mobile user experience – users expect to easily find what’s happening around them. For us, the iOS developers, there are few options to provide maps for the apps running on Apple devices. The most notable are Apple’s MapKit, Google Maps, MapBox. All of them have pros and cons and frequently in our apps’ life on the App Store, there’s a need to replace one map implementation with the other.
Since all of them have different classes, methods and architecture, this can be a tedious task. We will need to make a lot of code changes and adjustments, which might introduce bugs and affect the stability of the application. There has to be a better way.
Protocols to the rescue
Protocols are perfect for situations like this. We can define a protocol of what we expect of a map. We will define all the required methods and we will call those methods in our code. An implementation of our map protocol will be decoupled from the business logic of our app. Whenever there’s a need for a different solution, we will just implement the map protocol with the new SDK. Replacing the implementation will be trivial. Our goal in this post would be to implement protocol-oriented map, where our controllers will not know anything about the underlying implementation. The concrete map implementation will be done with MapBox.
MapBox is great map solution, which provides a lot of ways to create custom vector maps, which are rendered in real-time, really fast. It provides offline usage, drawing annotations, rectangles, custom objects, heatmaps and a lot more. Check the examples and tutorials on the MapBox website, there is a lot of good material there. We will use it in our application for displaying map pins for different types of businesses, like pharmacies, gas stations and shops in the beautiful town Ohrid. The app will enable filtering, where users can select what kind of map they want to use and which types of pins they want displayed.
It’s always best to start with a protocol. We will first define a protocol for the map view. The protocol contains method for creating a map, with a frame and initial coordinates. It also has method for updating the map, with the map type the user has selected and the entries (map pins), that have to be displayed on the map.
Next, let’s define a protocol that will take care of providing the data source of the map. We will call this MapService protocol.
It contains methods for providing the default map type, the supported map types (for example outdoors, streets map, satellite), supported entry types (like pharmacies, gas stations and shops), as well as the map entries, which represent the actual pins displayed on the map.
There are a lot of new types in these two protocols, which are all structs. There are many benefits of using value types like structs over classes, explained in this post.
For the MapType, we are defining the display name and the url where the map can loaded from. For the EntryType, we will also need a title and an image name, which will be the image of the pin that will be displayed on the map. Since we will need to compare different entry types, in order to check whether an entry type is contained in an array, we will need to implement the Equatable protocol. By implementing this protocol, we basically tell Swift which entry types should be considered equal. More details about this protocol in this excellent blog post.
The MapState struct represents what’s currently selected in the filtering screen – which map type and entry types. The MapEntry struct represents the actual pin on the map and contains information about the type of the entry, the title, the location where it should be displayed, as well as its id.
These structs and protocols would be the only things our app would be aware of. Let’s now see how the initial ViewController will utilise them.
In the viewDidLoad method, we are adding as subview the map that the MapView protocol implementation returns. Here we are not interested in (and don’t know) what type of map it is, we just expect a map as a UIView with the frame we are providing. We are also setting up the initial map state, with the default map that’s going to be displayed and which pin types will be initially selected.
When the user clicks the Filter button in the navigation bar, the FilterViewController is presented. The controller expects the current state of the map and provides delegate method when that state is updated. The map styles and pin types are displayed in separate sections of a grouped table view. For the map styles, only one selection is possible, whereas for the pin types, multiple selections can be performed.
The table view data source implementation follows. We are implementing the standard data source methods and the data source is our MapService protocol, which provides the table view with all the necessary data. Depending on whether it’s the first or the second section, we are providing either the map types or the supported entry types. Here, we are also not interested how the mapService is implemented, we are just expecting data from it in a predefined manner.
The table view delegate method implementation is also straightforward. Here we are enabling the single selection for the map type and the multiple selection for the map entry types. For the multiple selection, we are using the selectedEntryTypes array in the MapState variable. If the array already contains the entry, we are removing it, otherwise, we are adding it to the array.
One last thing we need to do is to inform the delegate that the state is updated. We are doing that in the viewWillDisappear method. Then, in the initial ViewController, we are implementing that method. We are saving the new state and we are telling the map view to update it’s contents based on the changes.
That’s everything we needed to do in our app to support maps. Notice, we haven’t mentioned, nor imported MapBox anywhere. Our protocol-based approach allowed us to do that.
Now, let’s provide the MapBox implementation of the two protocols. To get started with the SDK, you will need to add it to your Podfile and run pod install.
target 'MapBoxPlayground' do pod 'Mapbox-iOS-SDK', '~> 3.6' end
In order to use the MapBox SDK, you need to obtain mapbox access token, by creating an account on their website. The token should be added in your Info.plist file.
Now, the interesting part. Let’s define a new class, called MapBoxView, which will implement the MapView protocol, using the MapBox SDK.
We are implementing the map(withFrame:,initialCoordinates:) method by creating a new instance of the class MGLMapView from MapBox, which represents the map. We are setting the frame, the delegate, the initial coordinates and the zoom level.
The update(withMapType:,entries:) implementation is split in two methods, one for updating the map type and the other for the map entries. For the map type, we are checking whether the url is changed and only in that case, we are setting the new styleURL of the map.
For updating the map entries, we are doing several things. First, we are removing all the existing annotations. Then, we are going through the map entries and create MGLPointAnnotation object for each entry. This object represents an annotation on the map. We are setting all the required information about the coordinate and the title, and we are adding it to the list of annotations. We are also mapping the title of the annotation to the image name – that will make the job of setting the pin image easier for us. At the end, we are adding all annotation to the map view.
In order to show the appropriate image for every pin, we need to implement methods from the MGLMapViewDelegate.
The mapView(,imageFor:) delegate method is the place to customise the annotation image. Here we are taking the title of the annotation, we are using it as key in our imageInfo dictionary (which was filled in the update method above) and then we are loading the image from the bundle. MapBox supports caching of the images, so we are trying to dequeue it from the map view.
The other method, mapView(,annotationCanShowCallout:) is used if you want to show popup when the user clicks on a pin. The default popup presents the title of the annotation, which is good enough for us.
The final thing we need to do is to implement the MapService. The MapBoxService class does that.
The mock data is extracted in another class, MockDataService, which we can easily replace if we want to connect the map with real data.
We instantiate these classes in the ViewController. If we want to replace the implementation, we just need to instantiate different class that implements the required protocols. No other code changes are needed.
That’s everything for this post. The key take-away is to always start with a protocol. Don’t make your projects dependable on a third-party library which might disappear in the future (like Parse few years ago). When we define protocols, it’s much easier to replace the underlying implementation, without introducing code changes in other areas of your project, like we have seen in this project.
You can find the full source code here.