The final part of this video then is to display a map of the location formed by the Longitude and latitude of the place found in our search,
Before we do that, let's do a quick exploration of what it takes in SwiftUI to create a map. It is extremely easy.
Create a new SwiftUI file and call it MapView
Maps require the MapKit framework, so
xxxxxxxxxx
import MapKit
To display a map, we need a region to display and this is a class that is available from the MapKit framework called MKCoordinateRegion
.
A region requires a center
which is formed by creating a CLLocationCoordinate2D
object that requires a double value for both latitude
and longitude
along with a span
which is an MKCoordinateSpan
that provides a delta
value from which we want to extend our view from the center in both directions.
This must be created as a @State
property so let's create one by passing in some latitude
and longitude
of a known location and provide some fixed span value.
The coordinates for the Empire State Building are (40.7484445, -73.9894536).
xxxxxxxxxx
@State private var mapRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 40.7484445,
longitude: -73.9894536),
span:MKCoordinateSpan(
latitudeDelta: 0.2,
longitudeDelta: 0.2)
)
To present a map with this region, all we have to do is create a SwiftUI Map view passing in the coordinateRegion
as a binding.
xxxxxxxxxx
var body: some View {
Map(coordinateRegion: $mapRegion)
}
I would like to use this new view in my project, but the problem is that our coordinates will change so they cannot be fixed.
To solve this, create two Double properties for latitude and longitude.
xxxxxxxxxx
let longitude: Double
let latitude: Double
Fix the PreviewProvider
by providing those same values we used in our example.
xxxxxxxxxx
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(longitude: 40.7484445, latitude: -73.9894536)
}
}
The problem we have now is that we can pass in our values from our LocationFinderView
to this view, but we cannot use those as is to create the mapRegion
as it will not know about those values yet, What we need to do is to cut out the MKCoordinate
region for now and keep in on your clipboard and not assign it to the mapRegion state property. Instead, we just indicate that it will be, when initialized a type of MKCoordinateRegion
What we can do now is create an initializer.
The initializer will have two parameters, longitude
and latitude
as Doubles and Xcode automatically generates that for us and assigns the arguments to the struct properties upon initialization.
xxxxxxxxxx
init(longitude: Double, latitude: Double) {
self.longitude = longitude
self.latitude = latitude
}
This @State
property for our mapRegion
however cannot occur before we have our longitude and latitude though so we need to create that state property without providing the initial value.
xxxxxxxxxx
@State private var mapRegion: MKCoordinateRegion
Then in the initializer, once we have the longitude and latitude we can use that to construct our mapRegion and assign it to the struct property and use those values for the CLLocationCoordinate2D values..
xxxxxxxxxx
self,mapRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude),
span:MKCoordinateSpan(
latitudeDelta: 0.2,
longitudeDelta: 0.2))
We can return to our LocationFinderView now and use that MapKit if we get a valid locationInfo object.
Below the Text
view that presents the locationInfo.state
we can present our MapView
passing in the locationInfo.longitude
and locationInfo.latitude
.
Add some padding.
xxxxxxxxxx
MapView(longitude: locationInfo.longitude, latitude: locationInfo.latitude)
.padding()
If you test this now, you may find that it is working perfectly for you. However, run in the simulator and select Germany and enter the first postal code that they suggest in the range.
When you do this, the app crashes and you get:
It looks like the API is not perfect because what it has done is returned a longitude of 14612.00000000 and that is impossible. Both latitude and longitude degrees must be between -180 and +180 and our MapView cannot catch that error.
We MUST ensure that when we provide our SwiftUI a set of coordinates, they are actually valid coordinates.
Back in the LocationService
class, inside the fetchLocation
function we can check to see of our longitude and latitudes are within that range, and if not, create an error.
Below where we create our locationInfo
, create a range
property.
xxxxxxxxxx
let range = -180.0...180.0
Then we can check to see whether or not the locationInfo.latitude
and locationInfo.longitude
properties are within that range.
If not, we assign a string to our published errorString
property
Note that even though locationInfo is an optional property and it is normally not a good idea to force unwrap an object, in this case we know it exists because we have just made the assignment so we can safely do that here when we access the two properties
xxxxxxxxxx
if !(range.contains(locationInfo!.latitude) && range.contains(locationInfo!.longitude)) {
errorString = "Invalid map coordinates"
}
Finally, back in LocationFinderView
we will only present our mapView
if there is no locationService.errorString
, meaning it is still at nil
.
xxxxxxxxxx
if locationService.errorString == nil {
MapView(longitude: locationInfo.longitude, latitude: locationInfo.latitude)
.padding()
}
When we try to access that same code now from Germany, we get the error message, but we are also still getting the location and state.
There is one more thing that we have to do though and that is to make sure we reset the map if we choose to stay with the same country, yet change the Zip or Postal Code.
So in LocationFinderView, right after the onChange modifier for the selectedCountry, we create another onChange modifier that watches for change on the code that will preform a locationService.reset()
Now. Since our onChange modifier resets per code to an empty string, this will then initiate that onChange modifier for the code variable. This means we can remove the call to locationService.reset() in that method.
The final two onChange modifiers now should look like this.
xxxxxxxxxx
.onChange(of: selectedCountry) { _ in
code = ""
}
.onChange(of: code) { _ in
locationService.reset()
}