This project is going to be based around a map view, asking users to add places to the map that they want to visit. To do that we need to place a Map
so that it takes up our whole view, track its annotations, and also whether or not the user is viewing place details.
We’re going to start with a full-screen Map
view, giving it an initial position showing the UK – you're welcome to change that, of course!
First, add an extra import
line so we get access to MapKit’s data types:
import MapKit
Second, add a property inside ContentView
that will store the start position for the map:
let startPosition = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 56, longitude: -3),
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)
)
)
And now we can fill in the body
property
Map(initialPosition: startPosition)
If you run the app now you’ll see you can move the map around freely – if you want to change the map style, now is a good time to try using .mapStyle(.hybrid)
or similar, for example.
All this work by itself isn’t terribly interesting, so the next step is to let users tap on the map to add place marks. Previously we used buttons for handling screen taps, but in this instance we need something different called a tap gesture – a new modifier we can add to any view to trigger code when it's tapped by the user.
Important: Many SwiftUI developers over-use tap gestures, and it causes all sorts of problems for users who rely on screen readers. If possible, it's always a better idea to use a button or other built-in control rather than adding a tap gesture. In this case we have no choice but to use a tap gesture, because it tells us where on the map the user tapped.
Go ahead and modify the Map
to this:
Map(initialPosition: startPosition)
.onTapGesture { position in
print("Tapped at \(position)")
}
If you run the app again you'll see how the tap gesture doesn't interfere with the default map gestures – you can still pan around, pinch to zoom, and more.
However, the tap location isn't ideal because it gives us screen coordinates rather than map coordinates. To fix that, we need a MapReader
view around our map, so we can convert between the two types of coordinates.
Change your code to this:
MapReader { proxy in
Map(initialPosition: startPosition)
.onTapGesture { position in
if let coordinate = proxy.convert(position, from: .local) {
print("Tapped at \(coordinate)")
}
}
}
Where things get interesting is how we place locations on the map. We’ve bound the location of the map to a property in ContentView
, but now we need to send in an array of locations we want to show.
This takes a few steps, starting with a basic definition of the type of locations we’re creating in our app. This needs to conform to a few protocols:
Identifiable
, so we can create many location markers in our map.Codable
, so we can load and save map data easily.Equatable
, so we can find one particular location in an array of locations.In terms of the data it will contain, we’ll give each location a name and description, plus a latitude and longitude. We’ll also need to add a unique identifier so SwiftUI is happy to create them from dynamic data.
So, create a new Swift file called Location.swift, giving it this code:
struct Location: Codable, Equatable, Identifiable {
let id: UUID
var name: String
var description: String
var latitude: Double
var longitude: Double
}
Storing latitude and longitude separately gives us Codable
conformance out of the box, which is always nice to have. We’ll add a little more to that shortly, but it’s enough to get us moving.
Now that we have a data type where we can store an individual location, we need an array of those to store all the places the user wants to visit. We’ll put this into ContentView
for now just we can get moving, but again we’ll return to it shortly to add more.
So, start by adding this property to ContentView
:
@State private var locations = [Location]()
Next, we want to add a location to that whenever the onTapGesture()
is triggered, so replace its current code with this:
if let coordinate = proxy.convert(position, from: .local) {
let newLocation = Location(id: UUID(), name: "New location", description: "", latitude: coordinate.latitude, longitude: coordinate.longitude)
locations.append(newLocation)
}
Finally, update ContentView
so we create markers from each of the locations in the array:
Map(initialPosition: startPosition) {
ForEach(locations) { location in
Marker(location.name, coordinate: CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude))
}
}
That’s enough map work for now, so go ahead and run your app again – you should be able to move around as much as you need, then add locations wherever you see fit.
I know it took a fair amount of work to get set up, but at least you can see the basics of the app coming together!
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.