How to get current coordinate span in swiftUI? - swift

I'm working with a map kit map and I need to get the current value of the coordinate span model.
So that when you click on the map point, the map itself does not jump.
import SwiftUI
import MapKit
struct MapScreenView: View {
#StateObject private var vm = LocationsViewModel()
var body: some View {
ZStack {
VStack {
HStack {
ButtonFilterView()
Spacer()
}
Spacer()
}.zIndex(1)
Map(coordinateRegion:
$vm.mapRegion, annotationItems: vm.locations) {
location in
MapAnnotation(coordinate: location.coordinate) {
LocationMapAnnotationView()
.scaleEffect(vm.mapLocation == location ? 1.1 : 0.7)
.animation(.easeInOut, value: vm.mapLocation == location)
.onTapGesture {
vm.showTappedLocation(location: location)
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
struct MapScreenView_Previews: PreviewProvider {
static var previews: some View {
MapScreenView()
}
}
import Foundation
import MapKit
import SwiftUI
class LocationsViewModel: ObservableObject {
#Published var locations: [Location] = LocationDataService.locations
#Published var mapLocation: Location {
didSet {
updateMapRegion(location: mapLocation)
}
}
#Published var mapRegion = MKCoordinateRegion()
#Published var mapSpan = MKCoordinateSpan(latitudeDelta: 0.04, longitudeDelta: 0.04)
init() {
self.mapLocation = Location(name: "", coordinate: CLLocationCoordinate2D(latitude: 55.755864, longitude: 37.617698))
updateMapRegion(location: Location(name: "", coordinate: CLLocationCoordinate2D(latitude: 55.755864, longitude: 37.617698)))
}
private func updateMapRegion(location: Location) {
withAnimation(.easeInOut) {
mapRegion = MKCoordinateRegion(
center: location.coordinate,
/// here i want get current span value
span: location.name == "" ? MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) : mapSpan
)
}
}
func showTappedLocation(location: Location) {
mapLocation = location
}
}
When I click on the map pin, it returns to the hard code span.
How can I get current span?
I searched for many solutions, but did not find the right one

SwiftUI Map takes a binding to a MKCoordinateRegion.
In your code you have...
Map(coordinateRegion: $vm.mapRegion, ...
This is a twi way communication between the view model's region and the map view. When the map updates, this region updates. When you update the region, the map updates.
So the current values of your map are help inside that mapRegion.
Looking at the docs for MKCoordinateRegion https://developer.apple.com/documentation/mapkit/mkcoordinateregion/1452293-span
It has a property span which is an MKCoordinateSpan.
So in the view model... this is the current span of the map.
To update your function you could do something like...
private func updateMapRegion(location: Location) {
withAnimation(.easeInOut) {
mapRegion = MKCoordinateRegion(
center: location.coordinate,
/// here i want get current span value
span: mapRegion.span
)
}
}
Having said that, you seem to be creating a whole new region here. I don't know if that's a good idea. You could just update the center of the existing region without creating a whole new one.
private func updateMapRegion(location: Location) {
withAnimation(.easeInOut) {
mapRegion.center = location.coordinate
}
}

Related

Possible to change map type (for instance: satellite) in MapKit - SwiftUI

Is it possible with the newer MapKit, i.e using Map(), to change the map type to for instance satellite? I know that it is possible by using the old ways with UIviews and so on but where I get in trouble is when I want to use map annotations which are super simple in the new MapKit. (I want to loop through some CoreData an display the data on a map).
I actually tested the answer in this post SwiftUI: change map type on map view? and it worked when starting the app, but when I leaved the view where the map was in and then entered back to the view the maps was back to its standard view, not satellite.
Is there a simple solution to this or should I just live with it and hope for a change at this year WWDC?
Here is an example code (its from hackingWithSwift):
import SwiftUI
import MapKit
struct MapTypesView: View {
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.5, longitude: -0.12), span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
var body: some View {
Map(coordinateRegion: $mapRegion, annotationItems: locations) {
location in MapAnnotation(coordinate: location.coordinate){
NavigationLink {
Text(location.name)
} label: {
Circle()
.stroke(.red, lineWidth: 3)
.frame(width: 44, height: 44)
}
}
}
.onAppear {
MKMapView.appearance().mapType = .satellite
}
}
}
struct MapTypesView_Previews: PreviewProvider {
static var previews: some View {
MapTypesView()
}
}
struct Location: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
let locations = [
Location(name: "Buckingham Palace", coordinate: CLLocationCoordinate2D(latitude: 51.501, longitude: -0.141)),
Location(name: "Tower of London", coordinate: CLLocationCoordinate2D(latitude: 51.508, longitude: -0.076))
]
And below is my very simple contentView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink {
MapTypesView()
} label: {
Image(systemName: "map")
.imageScale(.large)
.foregroundColor(.accentColor)
}
}
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to add MKMapCamera and read current value?

I have a map with marks. When you press it, it centers. But when I rotate the map and click on the marker, it returns to its original value. I don't want the map to come back and the degrees stay the same.
I think that this can be solved through the camera, but I do not know how to do it.
I don't want the rotation to happen and the marker to just be centered. And rotation, if necessary, you can click on the compass in the upper right corner.
My code:
import SwiftUI
import MapKit
struct MapScreenView: View {
#StateObject private var vm = LocationsViewModel()
#State private var camera = MKMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 55.755864, longitude: 37.617698), fromDistance: 7500, pitch: 0, heading: 0)
var body: some View {
ZStack {
VStack {
HStack {
ButtonFilterView()
Spacer()
}
Spacer()
}.zIndex(1)
Map(coordinateRegion:
$vm.mapRegion, annotationItems: vm.locations) {
location in
MapAnnotation(coordinate: location.coordinate) {
LocationMapAnnotationView()
.scaleEffect(vm.mapLocation == location ? 1.1 : 0.7)
.offset(y: vm.mapLocation == location ? -11.5 : 0)
.animation(.easeInOut, value: vm.mapLocation == location)
.onTapGesture {
vm.showTappedLocation(location: location)
}
}
}
.onAppear {
MKMapView.appearance().isZoomEnabled = false
MKMapView.appearance().preferredConfiguration = MKStandardMapConfiguration(elevationStyle: .realistic)
MKMapView.appearance().pointOfInterestFilter = .some(MKPointOfInterestFilter.excludingAll)
MKMapView.appearance().isRotateEnabled = true
MKMapView.appearance().isPitchEnabled = true
MKMapView.appearance().isScrollEnabled = false
}
.edgesIgnoringSafeArea(.all)
}
}
}
struct MapScreenView_Previews: PreviewProvider {
static var previews: some View {
MapScreenView()
}
}
import Foundation
import MapKit
import SwiftUI
class LocationsViewModel: ObservableObject {
#Published var locations: [Location] = LocationDataService.locations
#Published var mapLocation: Location {
didSet {
updateMapRegion(location: mapLocation)
}
}
#Published var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 55.755864, longitude: 37.617698),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
#Published var mapSpan = MKCoordinateSpan(latitudeDelta: 0.04, longitudeDelta: 0.04)
init() {
self.mapLocation = Location(name: "", coordinate: CLLocationCoordinate2D(latitude: 55.755864, longitude: 37.617698))
updateMapRegion(location: Location(name: "", coordinate: CLLocationCoordinate2D(latitude: 55.755864, longitude: 37.617698)))
}
private func updateMapRegion(location: Location) {
withAnimation(.easeInOut) {
mapRegion.center = location.coordinate
}
}
func showTappedLocation(location: Location) {
mapLocation = location
}
}
I think that this can be solved through the camera, but I do not know how to do it.

Show users location on a struct MapView

I want to show the users location on a struct view of a MapView. Currently, the map is implemented with showing places fetched from an API.
Now I want to add the user location. It is done in a struct so that it can be called as a tabItem.
Most tutorials are showing the implementation in a class. Is there a simple way to add the user location to this struct?
import MapKit
import SwiftUI
import CoreLocation
struct MapView: View{
#EnvironmentObject var places: Places
#State var placesAPI = [Place]()
#State var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 51.3704, longitude: 6.1724),
span: MKCoordinateSpan(latitudeDelta: 5, longitudeDelta: 5)
)
var body: some View {
Map(coordinateRegion: $region, annotationItems: places) {
places in
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: places.latitude, longitude: places.longitude)) {
NavigationLink(destination: DiscoverView(place: places)) {
Image(places.city)
.resizable()
.cornerRadius(10)
.frame(width: 40, height: 20)
.shadow(radius: 3)
}
}
}
.navigationTitle("Locations")
.onAppear(){
apiCallPlaces().getPlaces{(places) in
self.places = places
}
}
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
If all you need is the to show it just use a different constructor for Map
https://developer.apple.com/documentation/mapkit/map
init(coordinateRegion: Binding<MKCoordinateRegion>, interactionModes: MapInteractionModes, showsUserLocation: Bool, userTrackingMode: Binding<MapUserTrackingMode>?)
Having showsUserLocation set to true will do the trick with the proper permissions.

SwiftUI Map Camera Movement

I am trying to have the map camera continuously animate around the globe. Currently I'm trying to change the coordinateRegions latitude in the onAppear when the map loads. Im using a timer to update the latitude every second. So far i'm getting the error "Modifying state during view update, this will cause undefined behavior; however the updated latitude is printing in the console but the map's coordinateRegion is not being updated." I was wondering if there's a better way to continuously animate the map around the globe, in swiftUI.
Appreciate any help,
Thanks
import SwiftUI
import MapKit
struct MapView: View {
var timer = Timer()
#State var startlocation = CLLocationCoordinate2DMake(41.851, -87.6238)
#State var endLatitude = 41.91 //North Avenue Beach
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 41.851, longitude: -87.6238),
span: MKCoordinateSpan(latitudeDelta: 40, longitudeDelta: 40)
)
func moveRegion() {
var currentLatitude = region.center.latitude
let increment = 0.00005
Timer.scheduledTimer(withTimeInterval: (1.0/30.0), repeats: true) { (timer) in
currentLatitude += increment
if currentLatitude >= self.endLatitude {
timer.invalidate()
}
region.center.latitude = currentLatitude
print(currentLatitude)
}
}
var body: some View {
Map(coordinateRegion: $region, annotationItems: sites) { location in
MapAnnotation(
coordinate: location.coordinate,
anchorPoint: CGPoint(x: 0.5, y: 0.5)
) {
VStack {
Circle()
.overlay(
Circle()
.stroke(location.color.opacity(0.8), lineWidth: 3))
.foregroundColor(location.color.opacity(0.5))
.frame(width: location.radius, height: location.radius)
}
}
}.onAppear {
moveRegion()
}
}
}

SwiftUI action when clicking on map annotation

When clicking a specific annotation in MapView I wish to add an overlay with info about the annotation.
What i am looking for is being able to update a variable and then call updateUIView() such that the map shows the overlay corresponding to the annotation that was clicked.
You can do it like this:
let places: [Place] = [
.init(name: "One", coordinate: .init(latitude: 56.951924, longitude: 24.125584)),
.init(name: "Two", coordinate: .init(latitude: 56.967520, longitude: 24.105760)),
.init(name: "Five", coordinate: .init(latitude: 56.9539906, longitude: 24.13649290000000))
]
#State var coordinateRegion = MKCoordinateRegion(center: .init(latitude: 54.6872, longitude: 25.2797), latitudinalMeters: 2000000, longitudinalMeters: 2000000)
var body: some View {
Map(coordinateRegion: $coordinateRegion, annotationItems: places) { (place) in
MapAnnotation(coordinate: place.coordinate) {
Button(action: { print(place.name) }, label: {
Text(place.name)
})
}
}
}
The way to achieve this is track the selected item:
#State private var results: [SearchResult] = []
#State private var selectedResult: UUID?
Your items should already be Identifiable, something like this:
struct SearchResult: Identifiable {
let id = UUID()
let mapItem: MKMapItem
}
Then when you iterate through your map annotations you can decide to generate the selected one, or non-selected one:
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: $trackUser, annotationItems: results) { result in
buildAnnotation(result, isSelected: result.id == selectedResult)
}
And when building the annotation, add a tap gesture recognizer to change the selected item, and center the map if desired:
private func buildAnnotation(_ result: SearchResult, isSelected: Bool) -> some MapAnnotationProtocol {
MapAnnotation(coordinate: result.mapItem.placemark.coordinate, anchorPoint: CGPoint(x: 0.5, y: 1.0)) {
MyCustomView(isSelected: isSelected)
.onTapGesture {
withAnimation {
selectedResult = result.id
region.center = result.mapItem.placemark.coordinate
}
}
}
}
The MyCustomView is just a normal SwiftUI View with body and you can pass in whatever parameters you want to use from the item it represents, including whether it's selected or not.