MapKit | Adding a Map View Programmatically | Swift + UIKit
A short series about working with iOS MapKit.
You’ve likely heard before that one of the best ways to solidify your knowledge of something is to explain it to someone else, so that’s the frame of mind I was in when developing this series. I was recently tasked with working and adding new features around Map views in a project. So I had to quickly get an understanding of some basic MapKit features and felt that it would be a good topic to write and record a short series that builds on each part to get the user to a completed end prototype.
This mini-series focuses on 3 things:
- Adding a MapView Programmatically [You are Here]
- Adding Custom Annotations to a Map
- Drawing a Route on a Map
If you are more of an audio/visual person there are videos for each section, that are available on YouTube. So jumping right in.
Project Setup
Open XCode and create a new iOS App project. The options I used for setting up my project were as follows.
Prepare Project to Work with Programmatic Layouts
There are some minor changes that need to be made to configure our project to work without using storyboards and instead rely on us creating our views programmatically.
Delete the storyboard from the project files.
Delete the storyboard references from the info.plist file.
Update the SceneDelegate file to work without the now-deleted storyboard. It is important in this part to make your window?.rootViewController equal to the ViewController you want to instantiate on load. I opted to use the default ViewController that XCode provides when you start a new project, but if you have decided to replace yours with something else make sure to change that line as well, which is line 22 in the picture.
Set the background colour of the ViewController to test that everything is wired up okay.
Okay, that wasn’t too difficult right. Next, we’re going to add a map to our view in ViewController.
Add Map to View
Import MapKit into ViewController, at top of the file, by adding the following line, right after “import UIKit”.
import UIKit
import MapKit
Create an MKMapView by adding these lines. Forcing dark mode is not necessary and was a preference, feel free to do what works for you. If you don’t want to force dark mode, delete the 3rd line.
let mapView : MKMapView = {
let map = MKMapView()
map.overrideUserInterfaceStyle = .dark
return map
}()
Create a new function that will handle setting up the constraints on our newly created map displaying it in full-screen.
private func setMapConstraints() {
view.addSubview(mapView)map.translatesAutoresizingMaskIntoConstraints = false
mapView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
mapView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
This function needs to be called from somewhere to run, so call it from ViewDidLoad.
Extend ViewController to be the delegate for the map (You can place extensions either in their own file or at the bottom of the ViewController (after the last closing brace), pick what works best for you. Just make sure you remember to set the delegate.
extension ViewController : MKMapViewDelegate {
//Do something
}//Set the delegate by adding the following line in viewDidLoad
mapView.delegate = self
Import GPX JSON
If you’re following along with this tutorial step-by-step and are trying to re-create my results. You're going to need to get your hand on a JSON file that has a collection of GPX coordinates if you don’t have any data to work with handy. If that’s the case you can download the resource files from GitHub and work with the supplied sample data.
Create a Model to Work with JSON
Create a new File, you can name it Model.swift, and this will be the model you map your JSON to. If all of this is unfamiliar, and you don’t have your own data to work with I recommend you check out my project files on GitHub.
Get JSON from File
For this project, I was using a static file that was saved into the project so I have to first extract this data from the file to be used.
func getJSON() -> Data? {if let path = Bundle.main.path(forResource: "data", ofType: "json") {
do {
let data = try String(contentsOfFile: path).data(using: .utf8)
print("🟢SUCCESS: JSON read successfully:\n\(data)")
return data
} catch {
print("🛑ERROR: Unable to read JSON file")
}
}
return nil
}
Parse JSON
Now that we have access to the JSON data from the file, we need to parse that JSON into a usable format, using the model created earlier.
func parseJSON(jsonData: Data) {
do {
routeData = try JSONDecoder().decode(Route.self, from: jsonData)
for feature in routeData?.features ?? [] {
let loc = CLLocation(latitude: feature.geometry.coordinates[1], longitude: feature.geometry.coordinates[0])
routeCoordinates.append(loc)
}
} catch {
print("🛑ERROR: Unable to parse JSON file")
print(error)
}
}
Putting It All Together
So we have been creating all the functions but how does it all fit together? All of these functions for the purposes of our project are being called in viewDidLoad in the following way.
//
// ViewController.swift
// Route
//
//import UIKit
import MapKitclass ViewController: UIViewController { var routeData : Route?
var routeCoordinates : [CLLocation] = [] let mapView : MKMapView = {
let map = MKMapView()
map.overrideUserInterfaceStyle = .dark
return map
}() override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
mapView.delegate = self
setMapConstraints()
if let routeJSON = self.getJSON() {
parseJSON(jsonData: routeJSON)
}
}
func setMapConstraints() {
view.addSubview(mapView)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
mapView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
func getJSON() -> Data? {
if let path = Bundle.main.path(forResource: "data", ofType: "json") {
do {
let data = try String(contentsOfFile: path).data(using: .utf8)
print("🟢 SUCCESS: JSON read successfully")
return data
} catch {
print("🛑 ERROR: Unable to read JSON")
}
}
return nil
}
func parseJSON(jsonData: Data) {
do {
routeData = try JSONDecoder().decode(Route.self, from: jsonData)
for feature in routeData?.features ?? [] {
let loc = CLLocation(
latitude: feature.geometry.coordinates[1],
longitude: feature.geometry.coordinates[0]
)
routeCoordinates.append(loc)
}
} catch {
print("🛑 ERROR: Unable to parse JSON")
}
}}extension ViewController : MKMapViewDelegate {
//DELEGATE FUNCTIONS
}
Next:
Good progress has been made and in the next article, we will go over how to add and customize annotations, which are the pins you can use to mark a point on a map.
Links
YouTube Series
GitHub