Custom map style in MapKit

21.3k Views Asked by At

I am looking for a way to implement a custom map style in iOS 7, just like you can do with Google Maps. I have found some posts saying that this is not possible with MapKit, but they are all posted a while back. To clarify, by style I am talking about custom colors and preferably also fonts. Example of custom Google Map style below.

enter image description here
(source: servendesign.com)

I would really prefer using MapKit for performance reasons, but if it is not supported I am open to using other frameworks as well. The ones that I have seen are MapBox and Cloudmade, and of course the Google Maps SDK.

Is there a way of doing it with MapKit? If not, what is the best way to go?

4

There are 4 best solutions below

4
Marco On BEST ANSWER

MKMapView does not expose the properties you're interested in customizing. The Google Maps SDK does support custom colors and icons for markers, which may be sufficient for your purposes.

Edit: Stay tuned for iOS 11, which may offer this level of customization.

1
incanus On

Another option is MBXMapKit, which is a MapBox library built atop Apple's MapKit, though it's geared for MapBox layers. It is separate from the MapBox iOS SDK, which is a ground-up rewrite meant to work like MapKit but not based on it.

9
dehlen On

MKMapView also offers the possibility to use custom tile overlays. Openstreetmap has a great list of tile servers you could use to get a custom map. Of course there is always the possibility to create your own tile overlay set. The process is described in the Openstreetmap wiki here.

A possible implementation in Swift could look like this:

1. Import MapKit

import MapKit

2. Add overlays to map

let overlayPath = self.mapViewModel.overlayURL
let overlay = MKTileOverlay(URLTemplate: overlayPath)
overlay.canReplaceMapContent = true
self.mapView.addOverlay(overlay)

3. Conform to MKMapViewDelegate

class ViewController: UIViewController, MKMapViewDelegate { ... }

4. Implement delegate method to use the correct renderer to display the tiles

func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
    guard let tileOverlay = overlay as? MKTileOverlay else {
        return MKOverlayRenderer(overlay: overlay)
    }
    return MKTileOverlayRenderer(tileOverlay: tileOverlay)
}

In the above example overlayURL is taken from the tile server list found on openstreetmap: OpenstreetMap Tile Servers.

For example if you would like to use the stamen map (which has a watercolor style) your url would look like:

let overlayURL = "http://tile.stamen.com/watercolor/{z}/{x}/{y}.jpg"

If you are searching for a dark-mode map you probably go best with Carto Dark: http://a.basemaps.cartocdn.com/dark_all/${z}/${x}/${y}.png.

See that the above URLs has no SSL support (HTTP). Therefore you will need to allow insecure HTTP requests to this specific URL by adding the App Transport Security Settings in your Info.plist. For further information have a look at this link.

0
Arjan On

I created a class that allows one to create a custom style.

This particular implementation looks like this:

enter image description here

It has one small drawback: when panning, new tiles are loaded with the default colors first, the overlay only kicks in after a fraction of a second. Otherwise it's serving me right.

struct CustomOverlayMap: UIViewControllerRepresentable { // SwiftUI representation
    func makeUIViewController(context: Context) -> CustomOverlayMapViewController {
        let x = CustomOverlayMapViewController()
        x.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        return x
    }
    
    func updateUIViewController(_ uiViewController: CustomOverlayMapViewController, context: Context) {
        uiViewController.mapView.frame = uiViewController.view.frame
    }
    
    typealias UIViewControllerType = CustomOverlayMapViewController
}

class CustomOverlayMapViewController: UIViewController, MKMapViewDelegate {
    let mapView = MKMapView()
    
    let wholeWorldPolygon = [
        (90, 0),
        (90, 180),
        (-90, 180),
        (-90, 0),
        (-90, -180),
        (90, -180)
    ].map {
        CLLocationCoordinate2D(latitude: $0.0, longitude: $0.1)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mapView.mapType = .standard
        mapView.showsTraffic = false
        mapView.showsPointsOfInterest = false
        mapView.showsBuildings = false
        mapView.pointOfInterestFilter = MKPointOfInterestFilter(excluding: [.nationalPark, .park])
        
        mapView.delegate = self
        
        let overlays = [polygonFrom(wholeWorldPolygon, title: "one"), polygonFrom(wholeWorldPolygon, title: "two"), polygonFrom(wholeWorldPolygon, title: "three")]
        mapView.addOverlays(overlays, level: .aboveRoads)
        
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.view.addSubview(mapView)
    }
    
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        guard(overlay is MKPolygon) else { return MKOverlayRenderer() }
        
        let renderer = MKPolygonRenderer.init(polygon: overlay as! MKPolygon)
        
        switch renderer.polygon.title {
            case "one":
                renderer.fillColor = .red
                
                if #available(iOS 16.0, *) {
                    renderer.blendMode = .saturation
                } else {
                    // Fallback on earlier versions
                }
                renderer.alpha = 1.0
                
            case "two":
                renderer.fillColor = .red
                
                if #available(iOS 16.0, *) {
                    renderer.blendMode = .lighten
                } else {
                    // Fallback on earlier versions
                }
                renderer.alpha = 1.0
                
            case "three":
                renderer.lineWidth = 1.0
                renderer.fillColor = .red
                
                if #available(iOS 16.0, *) {
                    renderer.blendMode = .color
                } else {
                    // Fallback on earlier versions
                }
                renderer.alpha = 1.0
                
            default: break
        }
        
        return renderer
    }
    
    func polygonFrom(_ points: [CLLocationCoordinate2D], title: String? = nil) -> MKPolygon {
        let result: MKPolygon = MKPolygon(coordinates: points, count: points.count)
        result.title = title
        
        return result
    }
}