Reputation: 937
I am building an app in which I have to highlight some countries dynamically in the world map.
In short I want to customize the whole view of ios maps as shown in the images. can this be done using MapKit or is there any other method. Thanks in advance
Upvotes: 22
Views: 8517
Reputation: 29
Highlight countries, SwiftUI version
Here is a geojson: https://github.com/datasets/geo-boundaries-world-110m/blob/main/countries.geojson
import SwiftUI
import CoreLocation
struct GeoJson: Codable {
let type: String
let features: [GeoJsonFeature]
}
struct GeoJsonFeature: Codable {
let type: String
let geometry: GeoJsonGeometry
}
struct GeoJsonGeometry: Codable {
let type: String
let coordinates: GeoJsonCoordinates
}
struct GeoJsonCoordinates: Codable {
var point: [Double]?
var line: [[Double]]?
var polygon: [[[Double]]]?
var multiPolygon: [[[[Double]]]]?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let point = try? container.decode([Double].self) {
self.point = point
return
}
if let line = try? container.decode([[Double]].self) {
self.line = line
return
}
if let polygon = try? container.decode([[[Double]]].self) {
self.polygon = polygon
return
}
if let multiPolygon = try? container.decode([[[[Double]]]].self) {
self.multiPolygon = multiPolygon
return
}
throw DecodingError.valueNotFound(Self.self, .init(codingPath: [], debugDescription: ""))
}
}
struct ContentView: View {
@State private var shapes: [PolygonShape] = []
var body: some View {
ZStack {
ForEach(shapes) { shape in
Path { path in
guard let firstPoint = shape.points.first else { return }
path.move(to: firstPoint)
for point in shape.points {
path.addLine(to: point)
}
path.closeSubpath()
}
.fill(shape.color)
}
}
.onAppear {
loadGeoJson()
}
.frame(width: 300, height: 200)
.border(Color.black, width: 1)
}
func loadGeoJson() {
guard let url = Bundle.main.url(forResource: "countries", withExtension: "geojson"),
let data = try? Data(contentsOf: url),
let geoJson = try? JSONDecoder().decode(GeoJson.self, from: data)
else {
return
}
for feature in geoJson.features {
let geometry = feature.geometry
let randomColor = Color(hue: Double.random(in: 0...1), saturation: 1, brightness: 1)
if geometry.type == "Polygon", let coordinates = feature.geometry.coordinates.polygon {
for polygon in coordinates {
addShape(polygon: polygon, color: randomColor)
}
}
if geometry.type == "MultiPolygon", let coordinates = feature.geometry.coordinates.multiPolygon {
for multiPolygon in coordinates {
for polygon in multiPolygon {
addShape(polygon: polygon, color: randomColor)
}
}
}
}
}
func addShape(polygon: [[Double]], color: Color) {
let polygonCoordinates: [CLLocationCoordinate2D] = polygon.map { coordinate in
CLLocationCoordinate2D(latitude: coordinate[1], longitude: coordinate[0])
}
let points: [CGPoint] = polygonCoordinates.map { coordinate in
coordinateToPoint(coordinate)
}
let shape = PolygonShape(points: points, color: color)
shapes.append(shape)
}
func coordinateToPoint(_ coordinate: CLLocationCoordinate2D) -> CGPoint {
let width = 300.0
let height = 200.0
let x = (coordinate.longitude + 180.0) * (width / 360.0)
let latitudeRadians = coordinate.latitude * .pi / 180.0
let n = log(tan((.pi / 4.0) + (latitudeRadians / 2.0)))
let y = (height / 2.0) - (width * n / (2.0 * .pi))
return CGPoint(x: x, y: y)
}
}
struct PolygonShape: Identifiable {
let id = UUID()
let points: [CGPoint]
let color: Color
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can also add: let properties: [String: AnyCodableGeoJson] to get more data from .geojson
struct GeoJsonFeature: Codable {
let type: String
let geometry: GeoJsonGeometry
let properties: [String: AnyCodableGeoJson]
}
Upvotes: 0
Reputation: 29
According to this data, Crimea belongs to Russia, which is not true.
Here is a geojson with countries coordinates: https://github.com/nvkelso/natural-earth-vector/blob/master/geojson/ne_110m_admin_0_countries.geojson that should fit the needs for the task in this question.
Correct .geojson: https://github.com/datasets/geo-boundaries-world-110m/blob/main/countries.geojson
Upvotes: 0
Reputation: 10308
I recently looked on some possibilities for implementing something similar and you could now actually implement something like this quite easily without additional libraries. To do that you need countries borders coordinates data and you can get that from Natural Earth (https://www.naturalearthdata.com/). This data is unfortunately in format that cannot be easily read on iOS but you can convert it to a json format or to be precise geojson format with QGIS (https://www.qgis.org/en/site/forusers/download.html) or you can just use geojson that someone else converted. Here is a geojson with countries coordinates: https://github.com/nvkelso/natural-earth-vector/blob/master/geojson/ne_110m_admin_0_countries.geojson that should fit the needs for the task in this question.
We can parse geojson file and draw a map with CAShapeLayers. Below is an example code that draws a map with random colors for shapes from geojson which gave me the result as on the screenshot below which I think is quite close to expected result so modifying it should be quite easy.
Geojson parsing and map drawing code example:
import UIKit
import CoreLocation
struct GeoJson: Codable {
let type: String
let features: [GeoJsonFeature]
}
struct GeoJsonFeature: Codable {
let type: String
let geometry: GeoJsonGeometry
}
struct GeoJsonGeometry: Codable {
let type: String
let coordinates: GeoJsonCoordinates
}
struct GeoJsonCoordinates: Codable {
var point: [Double]?
var line: [[Double]]?
var polygon: [[[Double]]]?
var multiPolygon: [[[[Double]]]]?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let point = try? container.decode([Double].self) {
self.point = point
return
}
if let line = try? container.decode([[Double]].self) {
self.line = line
return
}
if let polygon = try? container.decode([[[Double]]].self) {
self.polygon = polygon
return
}
if let multiPolygon = try? container.decode([[[[Double]]]].self) {
self.multiPolygon = multiPolygon
return
}
throw DecodingError.valueNotFound(Self.self, .init(codingPath: [], debugDescription: ""))
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
loadGeoJson()
}
func loadGeoJson() {
guard let url = Bundle.main.url(forResource: "ne_110m_admin_0_countries", withExtension: "geojson"),
let data = try? Data(contentsOf: url),
let geoJson = try? JSONDecoder().decode(GeoJson.self, from: data)
else {
return
}
for feature in geoJson.features {
let geometry = feature.geometry
let randomColor = UIColor(hue: Double.random(in: 0...1), saturation: 1, brightness: 1, alpha: 1)
// check https://macwright.com/2015/03/23/geojson-second-bite.html for other types info if needed
// note that below we do not support it exactly as it should (internal cutouts in polygons are ignored)
// but for needed purpose it should not make a big difference
if geometry.type == "Polygon", let coordinates = feature.geometry.coordinates.polygon {
for polygon in coordinates {
addShape(polygon: polygon, color: randomColor)
}
}
if geometry.type == "MultiPolygon", let coordinates = feature.geometry.coordinates.multiPolygon {
for multiPolygon in coordinates {
for polygon in multiPolygon {
addShape(polygon: polygon, color: randomColor)
}
}
}
}
}
func addShape(polygon: [[Double]], color: UIColor) {
let polygonCoordinates: [CLLocationCoordinate2D] = polygon.map { coordinate in
CLLocationCoordinate2D(latitude: coordinate[1], longitude: coordinate[0])
}
let points: [CGPoint] = polygonCoordinates.map { coordinate in
coordinateToPoint(coordinate)
}
let path = UIBezierPath()
path.move(to: points[0])
for point in points {
path.addLine(to: point)
}
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color.cgColor
shapeLayer.position = CGPoint(x: 50, y: 200)
view.layer.addSublayer(shapeLayer)
}
func coordinateToPoint(_ coordinate: CLLocationCoordinate2D) -> CGPoint {
let width = 300.0
let height = 200.0
let x = (coordinate.longitude + 180.0) * (width / 360.0)
let latitudeRadians = coordinate.latitude * .pi / 180.0
let n = log(tan((.pi / 4.0) + (latitudeRadians / 2.0)))
let y = (height / 2.0) - (width * n / (2.0 * .pi))
return CGPoint(x: x, y: y)
}
}
Upvotes: 1
Reputation: 5128
You want to look into the Mapbox iOS SDK which will allow you to do this and more with a MapKit-like API. In particular, you will want to quickly make a custom map with TileMill using the provided Natural Earth data set for world country borders, enable UTFGrid interactivity so that the tapped regions can be identified, and use the RMShape
class on an RMAnnotation
onto the map view to add/color country polygons as needed. This sounds a little complex but the tools exist, are entirely free and open source, and I can help you with this process.
Upvotes: 9