Hello everyone I'm experiencing issues with my map implementation in test Swift App. So I added view for map just like in the code below, then as preview data I passed coordinates of a random city. Then I ran live preview, and (with lot of disappointment) saw blue rectangle instead of map. What have I messed up?
import MapKit
struct MapSection: View {
private var log: Log?
#State var region: MKCoordinateRegion = MKCoordinateRegion()
private var locationCoordinate: CLLocationCoordinate2D?
init(_ log: Log) {
self.log = log
if let coordinate = log.location?.coordinate {
let locationCoordinate = CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude)
self.locationCoordinate = locationCoordinate
self.region = MKCoordinateRegion(center: locationCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.075, longitudeDelta: 0.075))
} else {
self.log = nil
self.locationCoordinate = nil
}
}
var body: some View {
Section(header: Text("Position on map")) {
if log != nil && locationCoordinate != nil {
Map(coordinateRegion: $region, annotationItems: [self.log!]) {
_ in MapPin(coordinate: locationCoordinate!)
}
.frame(minHeight: 500)
}
else {
HStack {
Spacer()
Text("Map Error: Position Unknown")
Spacer()
}
}
}
}
}
struct LogDetail_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let log = Log(context: context)
log.country = Locale.current.regionCode
log.date = Date(timeIntervalSince1970: TimeInterval(100000))
log.location = CLLocation(latitude: CLLocationDegrees(52.229676), longitude: CLLocationDegrees(21.012229))
return LogDetail(log)
}
}
Related
I pulled the data from the mapmarker, when I print, I see all of them, but I cannot get them from the example ( quake.latitude ). Error. Even if it was a list, I had taken it by typing "(quakes) { quake in" in parentheses, but now I don't know how to do it through mapview.
struct MapAnnotationsView: View {
#State var quakes: [EarthQuake] = []
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.9520281, longitude: 35.6980142), span: MKCoordinateSpan(latitudeDelta: 30, longitudeDelta: 10))
let placeArray: [Place] = [Place(title: {quake.latitude}, coordinate: CLLocationCoordinate2D(latitude: 37.8008, longitude: 27.2465))]
var body: some View {
Map(coordinateRegion: $region, annotationItems: placeArray) { annotation in
// This makes a generic annotation that takes a View
MapAnnotation(coordinate: annotation.coordinate) {
// This is your custom view
AnnotationView(placeName: annotation.title)
}
} .onAppear {
Api().getEarthQuake { (quakes) in
self.quakes = quakes
}
}
}
}
Error code screenshot
Try this:
struct MapAnnotationsView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.9520281, longitude: 35.6980142), span: MKCoordinateSpan(latitudeDelta: 30, longitudeDelta: 10))
#State private var placeArray: [Place] = []
var body: some View {
Map(coordinateRegion: $region, annotationItems: placeArray) { annotation in
// This makes a generic annotation that takes a View
MapAnnotation(coordinate: annotation.coordinate) {
// This is your custom view
AnnotationView(placeName: annotation.title)
}
} .onAppear {
Api().getEarthQuake { (quakes) in
let tempArray = quakes.map{ quake in
Place(title: "\(quake.latitude)", coordinate: CLLocationCoordinate2D(latitude: 37.8008, longitude: 27.2465))
}
self.placeArray = tempArray
}
}
}
}
As this is not a reproducible example there might be some typos involved here which you would have to address yourself.
Explenation:
Your property placeArray cannot depend on another property. To get an array of places to provide to your map you need to create these after you loaded the earthquakes from your API and assign them. Using a #State var ensures your View gets updated.
I have a simple map made with "pure" SwiftUI. There is a search bar to search a place and when I click the "Go" button it shows the instructions of how to go that place from a particular location. It shows it below the map, on the "Enter a destination" field. What I want to do is, I want these instructions to be clickable. When I click each of the instructions it should zoom in that particular place where the instruction takes place. Right now it's only a list of text. Is it possible to do it without using UIViewRepresentable? And how can I do it?
I tried with
.onTapGesture {
region.span = MKCoordinateSpan(latitudeDelta: region.span.latitudeDelta/2, longitudeDelta: region.span.longitudeDelta/2)
}
but it zooms in the same location on every instruction I click.
ContentView
struct Location: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
struct RouteSteps: Identifiable {
let id = UUID()
let step: String
}
struct ContentView: View {
#State private var searchBar: String = ""
#State private var home = CLLocationCoordinate2D(latitude: 39.90068, longitude: 32.86081)
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 39.90068, longitude: 32.86081), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
#State var routeSteps: [RouteSteps] = [RouteSteps(step: "Enter a destination")]
#State var annotations = [Location(name: "Ankara", coordinate: CLLocationCoordinate2D(latitude: 39.90068, longitude: 32.86081))]
var body: some View {
VStack{
HStack {
TextField("", text: $searchBar)
Button("Go") {
findNewLocation()
}
.frame(width: 35, height: 35)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}.textFieldStyle(.roundedBorder).colorInvert()
Map(coordinateRegion: $region, annotationItems: annotations){ item in
MapMarker(coordinate: item.coordinate)
}.frame(width: 400, height: 300)
List(routeSteps) { r in
Text(r.step)
}
route function in ContentView
func findNewLocation(){
let searchResult = searchBar
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(searchResult, completionHandler:
{(placemarks, error) -> Void in
if((error) != nil){
print("error at geocode")
}
if let placemark = placemarks?.first {
let coordinates : CLLocationCoordinate2D = placemark.location!.coordinate
region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
annotations.append(Location(name: placemark.name!, coordinate: coordinates))
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: home, addressDictionary: nil))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: coordinates, addressDictionary: nil))
request.requestsAlternateRoutes = false
request.transportType = .automobile
let directions = MKDirections(request: request)
directions.calculate(completionHandler: { response, error in
for route in (response?.routes)! {
self.routeSteps = []
for step in route.steps {
self.routeSteps.append(RouteSteps(step: step.instructions))
}
}
})
}
})
}
Yes, but you have to stop throwing away the data. MKRoute.Steps actually gives you all of the information you need, so first, set up RouteSteps to accept MKRoute.Steps:
struct RouteSteps: Identifiable {
let id = UUID()
let step: MKRoute.Step
}
Then, in your findNewLocation() function:
func findNewLocation(){
...
directions.calculate(completionHandler: { response, error in
// I used a guard statement to get rid of your force unwrap.
// NEVER force unwrap in a situation like this.
// Implement error handling here
guard let response = response else { return }
for route in response.routes {
// Map the MKRoute.Steps to your struct
self.routeSteps = route.steps.map( { RouteSteps(step: $0) } )
}
})
...
}
Then in the main part of your ContentView:
struct ContentView: View {
...
var body: some View {
...
List(routeSteps) { routeStep in
// Pull your instructions out here
Text(routeStep.step.instructions)
.onTapGesture {
// The MKRoute.Step contains an overlay polyline for MKMapKit.
// We can't use that in SwiftUI maps yet, but the polyline contains a
// boundingMapRect that we can use to change the region shown on the map
// with the MKCoordinateRegion(_ MKMapRect) initializer. Setting the region
// to the insturction's polyline boundingMapRect does exactly what you want.
region = MKCoordinateRegion(routeStep.step.polyline.boundingMapRect)
}
}
...
}
I'm trying to use the new Map in SwiftUI. I would like to change the visible region
programmatically to include all of the annotations that are added programmatically,
similar to the way you can show a bounding box for a route overlay in the older UIMap.
I have not found anything in the documentation to do this. I created my own approach
by getting the min/max values for the latitude and longitude of each annotation and
then creating a center for the new region and a span. Both of these involve some math.
My result works for my North America location, but I will need to add significant
complexity to handle the cases where the annotation area encompasses the equator, the
date line or the prime meridian. Before I do so, I'm hoping someone has a solution
that I have missed.
Here is my solution:
struct MyMapView: View {
#StateObject var myMapVM = MyMapViewModel()
var body: some View {
VStack {
let m = Map(coordinateRegion: $myMapVM.region, annotationItems: myMapVM.centers) { site in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: site.lat, longitude: site.long)) {
GroupAnnotationPinView(title: site.name)
.onTapGesture {
openMapWithCoordinate(coordinate: CLLocationCoordinate2D(latitude: site.lat, longitude: site.long), name: site.name)
}//on tap
}//map annotation
}//map
m.onAppear {
//this works, so reference to Map seems to work ok
//print(m.body)
}
Button(action: {
myMapVM.createCoordinateRegion()
}, label: {
Image(systemName: "square.and.pencil")
.resizable()
.frame(width: 44, height: 44)
.padding()
})
.padding(.bottom, 20)
}//v
.ignoresSafeArea()
}//body
func openMapWithCoordinate(coordinate: CLLocationCoordinate2D, name: String) {
let place = MKPlacemark(coordinate: coordinate)
let mapItem = MKMapItem(placemark: place)
mapItem.name = name
mapItem.openInMaps(launchOptions: nil)
}//open
}//my map view
struct GroupAnnotationPinView: View {
#State private var showTitle = true
let title: String
var body: some View {
VStack(spacing: 0) {
Image(systemName: "mappin")
.font(.title)
.foregroundColor(.red)
}//v
}//body
}//group anno view
And the View Model
class MyMapViewModel: ObservableObject {
#Published var annotations: [MKAnnotation] = []
#Published var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.334_900,
longitude: -122.009_020),
latitudinalMeters: 10000,
longitudinalMeters: 10000
)
let centers: [Center] = [
.init(name: "One", lat: 37.334, long: -122.009),
.init(name: "Two", lat: 37.380, long: -122.010),
.init(name: "Three", lat: 37.400, long: -122.010),
.init(name: "Four", lat: 40.000, long: -120.000)
//.init(name: "Four", lat: 37.600, long: -121.800)
]
func createCoordinateRegion() {
//you need to fix this to account for dateline, prime meridian and equator in span
let maxX = centers.max(\.lat)
let maxY = centers.max(\.long)
let minX = centers.min(\.lat)
let minY = centers.min(\.long)
//print("minX.lat is ", minX?.lat ?? "nil")
//print("maxX.lat is ", maxX?.lat ?? "nil")
//print("minY.long is ", minY?.long ?? "nil")
//print("maxY.long is ", maxY?.long ?? "nil")
guard let minXS = minX?.lat, let maxXS = maxX?.lat, let minYS = minY?.long, let maxYS = maxY?.long else { return }
let deltaX = maxXS - minXS
let deltaY = maxYS - minYS
let newCenterLat = minXS + deltaX / 2
let newCenterLong = maxYS - abs(deltaY / 2)
//print(newCenterLat)
//print(newCenterLong)
let newRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: newCenterLat, longitude: newCenterLong), span: MKCoordinateSpan(latitudeDelta: max(deltaX, deltaY), longitudeDelta: max(deltaX, deltaY)))
region = newRegion
}//create coord region
}//class
extension Sequence {
func max<T: Comparable>(_ predicate: (Element) -> T) -> Element? {
self.max(by: { predicate($0) < predicate($1) })
}
func min<T: Comparable>(_ predicate: (Element) -> T) -> Element? {
self.min(by: { predicate($0) < predicate($1) })
}
}// ext seq
struct Center: Identifiable, Hashable {
let id = UUID()
let name: String
let lat: Double
let long: Double
}//center
Any guidance would be appreciated: Xcode 13.2.1 iOS 15.2
I do this by converting each annotation coordinate to an MKMapRect:
let rect = MKMapRect(origin: MKMapPoint(coordinate), size: MKMapSize(width: 0, height: 0))
MKMapRect is able to calculate a union and knows how to handle the 180th Meridian (see method spans180thMeridian:
private(set) var union: MKMapRect? = nil
func union(_ rect: MKMapRect) {
guard let union = self.union else {
self.union = rect
return
}
self.union = union.union(rect)
}
After you're done, you can convert it into a region and use it:
let region = MKCoordinateRegion(union)
Be aware that this cuts the AnnotationViews at the edge in half, so you have to calculate some space around it.
Alternative solution:
This is one of MANY reasons to prefer the old MKMapKit where you just call
mapView.showAnnotations(specificAnnotations, animated: true)
This changes the region to show specificAnnotations while keeping the other annotations on the map and not cutting AnnotationViews in half.
I am setting up a MapView with custom coordinates. Here is my code thus far
var coordModel: AirportModel?
struct MapView: View {
#Binding private var lat: Double
#Binding private var lon: Double
private let initialLatitudinalMetres: Double = coordModel?.airportLat ?? 0
private let initialLongitudinalMetres: Double = coordModel?.airportLong ?? 0
#State private var span: MKCoordinateSpan?
init(lat: Binding<Double>, lon: Binding<Double>) {
_lat = lat
_lon = lon
}
private var region: Binding<MKCoordinateRegion> {
Binding {
let centre = CLLocationCoordinate2D(latitude: lat, longitude: lon)
if let span = span {
return MKCoordinateRegion(center: centre, span: span)
} else {
return MKCoordinateRegion(center: centre, latitudinalMeters: initialLatitudinalMetres, longitudinalMeters: initialLongitudinalMetres)
}
} set: { region in
lat = region.center.latitude
lon = region.center.longitude
span = region.span
}
}
var body: some View {
Map(coordinateRegion: region)
}
}
In the AirportModel, there is a piece of data decoded in Double form called "latitude" and "longitude." I am simply trying to pass these data into the view after they are decoded. Here is my view body:
var body: some View {
ScrollView{
MapView(lat: <#Binding<Double>#>, lon: <#Binding<Double>#>)
.frame(height: 250)
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
I have tried putting coordModel?.latitude and coordModel?.longitude in for lat and lon, as well as setting variables above the call. I'm not quite sure where to go now.
Note: I am decoding a bunch of JSON data and every user query results in a different lat/lon pull (over 10000 potential responses). Thanks again!
Use a model that is an ObservableObject:
import Combine
import MapKit
class MapModel: ObservableObject {
var region: MKCoordinateRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.687330584, longitude: 9.219832454),
latitudinalMeters: 1000000,
longitudinalMeters: 1000000
) {
willSet {
self.objectWillChange.send()
}
}
// MARK: - test code change center evey 10 seconds
init() {
self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
self.testIndex += 1
self.testIndex %= self.testCoordinates.count
let newCenter = self.testCoordinates[self.testIndex]
let oldSpan = self.region.span
DispatchQueue.main.async {
self.region = MKCoordinateRegion(center: newCenter, span: oldSpan)
}
}
}
let testCoordinates = [
CLLocationCoordinate2D(latitude: 48.687330584, longitude: 9.219832454), // STR
CLLocationCoordinate2D(latitude: 41.297445, longitude: 2.0832941), // BCN
]
var testIndex: Int = 0
var timer: Timer? = nil
}
then create a View that observes your model
import SwiftUI
import MapKit
struct ContentView: View {
#ObservedObject private var mapModel = MapModel()
var body: some View {
Map(coordinateRegion: $mapModel.region)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
For test reasons I used a timer that simulates updated data from a server.
If you need Annotations, add them to the model and don't forget self.objectWillChange.send() if you change them.
How can I add a simple pin on my map with Xcode 11 GM / SwiftUI.
My code is as follows (here it shows me the map centered with the coordinates) but I want to show there only one pin of other coordinates.
import SwiftUI
import MapKit
struct ContentView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(
latitude: 34.011_286, longitude: -116.166_868)
let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I would appreciate any suggestions, thanks.
Update your code:
struct ContentView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
// 1
view.mapType = MKMapType.standard // (satellite)
// 2
let mylocation = CLLocationCoordinate2D(latitude: -6.863190,longitude: -79.818250)
// 3
let coordinate = CLLocationCoordinate2D(
latitude: -6.864138, longitude: -79.819634)
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
// 4
let annotation = MKPointAnnotation()
annotation.coordinate = mylocation
annotation.title = "My Location"
annotation.subtitle = "Visit us soon"
view.addAnnotation(annotation)
}
}
Found this post looking for how to add a simple pin/marker to XCode's MapKit and SwiftUI tutorials. Sharing a workable answer for using the new Map SwiftUI View rather than the more complex MKMapView in OP's question.
import SwiftUI
import MapKit
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
struct MapView: View {
var coordinate: CLLocationCoordinate2D
#State private var region = MKCoordinateRegion()
#State var markers = [Marker(location: MapMarker(coordinate: CLLocationCoordinate2D(latitude: -25.342863, longitude: 131.036974), tint: .blue))]
var body: some View {
Map(coordinateRegion: $region, annotationItems: markers) { marker in
marker.location }
.onAppear {
setRegion(coordinate)
}
}
private func setRegion(_ coordinate: CLLocationCoordinate2D) {
region = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
)
markers = [Marker(location: MapMarker(coordinate: coordinate, tint: .blue))]
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(coordinate: CLLocationCoordinate2D(latitude: -25.342863, longitude: 131.036974))
}
}