allow changing zoom of MapKit map, but from a Picker() - swift

ive got a screen with a map as a background, ive disabled zooming by doing UserInteraction: .pan. But i need to allow people to select the zoom level of their map by selecting a value from a picker.
Ive got the code for the picker and map done but I need to find a way to update the map when the zoom level is updated.
Basicly im just wondering how to programmicly change the zoom level of a MapKit map.
The map code:
Map(coordinateRegion: $viewModel.region,
interactionModes: .pan,
showsUserLocation: showsUser,
userTrackingMode: $tracking,
annotationItems: MapLocations,
annotationContent: { location in
MapAnnotation(
coordinate: location.coordinate,
content: {
ZStack{
Circle()
.fill(.blue)
.opacity(0.2)
.frame(width: CGFloat(setWidth), height: CGFloat(setHeight))
.overlay(
RoundedRectangle(cornerRadius: CGFloat(radius))
.stroke(Color.white, lineWidth: 5)
)
.animation(.default)
Circle()
.fill(.red)
.opacity(0.4)
.frame(width: CGFloat(nextWidth), height: CGFloat(nextWidth))
.overlay(
RoundedRectangle(cornerRadius: CGFloat(radius))
.stroke(Color.white, lineWidth: 2)
)
.animation(.default)
}
}
)
}
$viewModel. code (separate file):
final class MapViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
static let zoomLevel: Double = (UserDefaults.standard.double(forKey: "zoomLevel"))
static let startingLocation = CLLocationCoordinate2D(latitude: 37.33, longitude: -0-121.89)
static let defualtSpan = MKCoordinateSpan(latitudeDelta: zoomLevel, longitudeDelta: zoomLevel)
#Published var region = MKCoordinateRegion(center: startingLocation, span: defualtSpan)
var locationManager: CLLocationManager?
func checkIfLocationServicesIsEnabled() {
if CLLocationManager.locationServicesEnabled() {
locationManager = CLLocationManager()
locationManager!.delegate = self
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
}else{
print("Location is not enabled, please enable")
}
}
private func checkLocationAuthorization() {
guard let locationManager = locationManager else { return }
switch locationManager.authorizationStatus{
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
print("your location is restricted likely due to parental controls")
case .denied:
print("You have denied this app to use your location, please change it in settings.")
case .authorizedAlways, .authorizedWhenInUse:
region = MKCoordinateRegion(center: locationManager.location!.coordinate, span: MapViewModel.defualtSpan)
#unknown default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
}
Picker code(same file as first one):
Picker("title2", selection: $selectedZoom) {
ForEach(zoomLevels, id: \.self) { zoomLevel in
Text(zoomLevel)
}}
.pickerStyle(.segmented)
.frame(width: 300)
.onChange(of: selectedZoom) { _ in
if selectedZoom == "0.01"{
UserDefaults.standard.set(0.01, forKey: "zoomLevel")
UserDefaults.standard.set("0.01", forKey: "zoomName")
UserDefaults.standard.set("0.01 (Normal)", forKey: "zoomDesc")
zoomDescription = "0.01 (Normal)"
print("1")
}
if selectedZoom == "0.02"{
UserDefaults.standard.set(0.02, forKey: "zoomLevel")
UserDefaults.standard.set("0.02", forKey: "zoomName")
UserDefaults.standard.set("0.02 (Zoomed)", forKey: "zoomDesc")
zoomDescription = "0.02 (Zoomed)"
print("2")
}
if selectedZoom == "0.03"{
UserDefaults.standard.set(0.03, forKey: "zoomLevel")
UserDefaults.standard.set("0.03", forKey: "zoomName")
UserDefaults.standard.set("0.03 (Really Zoomed)", forKey: "zoomDesc")
zoomDescription = "0.03 (Really Zoomed)"
print("3")
}
if selectedZoom == "0.04"{
UserDefaults.standard.set(0.04, forKey: "zoomLevel")
UserDefaults.standard.set("0.04", forKey: "zoomName")
UserDefaults.standard.set("0.04 (Normal)", forKey: "zoomDesc")
zoomDescription = "0.04 (Normal)"
print("4")
}
if selectedZoom == "0.05"{
UserDefaults.standard.set(0.05, forKey: "zoomLevel")
UserDefaults.standard.set("0.05", forKey: "zoomName")
UserDefaults.standard.set("0.05 (SUper zoom)", forKey: "zoomDesc")
zoomDescription = "0.05 (SUper zoom)"
print("5")
}
}
Let me know if you need anything else, thanks!

Here is a very simple example of what you could possible do
import SwiftUI
import MapKit
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Map(coordinateRegion: $viewModel.region)
Picker("Zoom", selection: $viewModel.selectedZoom) {
ForEach(viewModel.zoomLevels, id: \.self) { zoomLevel in
Text("\(zoomLevel)")
}}
.pickerStyle(.segmented)
.frame(width: 300)
}
.padding()
}
}
class ViewModel: ObservableObject {
#Published var region: MKCoordinateRegion = .init(center: .init(latitude: 51.5072, longitude: 0.1276), latitudinalMeters: 500, longitudinalMeters: 500)
#Published var selectedZoom: Int = 1 {
didSet {
updateRegion(for: selectedZoom)
}
}
let zoomLevels = [1, 2, 3, 4, 5]
private func updateRegion(for zoom: Int) {
let center = region.center
let distance = getMeters(for: zoom)
region = MKCoordinateRegion(center: center, latitudinalMeters: distance, longitudinalMeters: distance)
}
private func getMeters(for zoom: Int) -> CLLocationDistance {
500 / Double(zoom)
}
}

You code is a bit too confusing to understand but i made a simple example where a stepper is used to update the zoom value of the map.
struct MaptestVIew: View {
#State var region: MKCoordinateRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 0, longitude: 0), span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
#State var zoomScale : Double = 1
let baseZoom : Double = 10
var body: some View {
VStack{
Map(coordinateRegion: $region, interactionModes: .pan)
Stepper(value: $zoomScale, in: 1...10) {
Text("Zoom")
}.onChange(of: zoomScale){ newZoom in
region = MKCoordinateRegion(center: region.center, span: MKCoordinateSpan(latitudeDelta: newZoom * baseZoom, longitudeDelta: newZoom * baseZoom))
}
}
}
}
When the stepper changes value the span for the map region is updated. You would probably want to update your MKCoordinateRegion span to make is zoom in/out

Related

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.

How to get current coordinate span in swiftUI?

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
}
}

How to properly put 2000+ Custom Annotations on SwiftUI Map():View to keep lag to minimum

I was curious about what is the best practice when you need to have a bunch of custom annotations on Map in SwiftUI. This is my first real IOS project, so I'm a little rough around the edges.
Currently, I have 2400 annotations, they are buttons with custom images and eventually, when the user clicks on them they will pop up information about the art piece.
What I did so far was put all the Data of each pin in a CoreData with the proper relationships to the entity it represents and an image String to call in my asset folder directly from the MapAnnotation content.
I have run it on my own iPhone 8. Fps drops significantly. I'm not sure if it's because the amount of annotations is very big, but that will not go down. Here's an example of my MapView()
FYI, I fetch the pins onAppear() and on the buttons will be a filter also. The code is far from done, just trying to fix one issue before going too far to come back.
Thanks for any input. Have a great day
import SwiftUI
import MapKit
import CoreData
struct MapView: View {
#Environment(\.managedObjectContext) private var moc
#State var pins: [Pin] = []
#State var userTrackingMode: MapUserTrackingMode = .follow
// Location for Montreal downtown
#State var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 45.50240, longitude: -73.57067),
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
var body: some View {
ZStack{
Map(coordinateRegion: $region,
interactionModes: MapInteractionModes.all,
showsUserLocation: true,
userTrackingMode: $userTrackingMode,
annotationItems: pins)
{ pin in
MapAnnotation(coordinate: CLLocationCoordinate2D(
latitude: pin.location?.latitude ?? 0,
longitude: pin.location?.longitude ?? 0))
{
Button(action: {
}){
if pin.isArtwork {
Image("\(pin.imageDefault!)")
.resizable()
.scaledToFit()
.frame(width: 10)
} else if pin.isPlace {
Image("\(pin.imageDefault ?? "place_pin")")
.resizable()
.scaledToFit()
.frame(width: 10)
}
}
}
}
.accentColor(Color.blue)
HStack(alignment: .bottom){
Spacer()
VStack(alignment: .trailing){
Spacer()
Button(action: {userTrackingMode = .follow}) {
Image("user_location")
.resizable()
.scaledToFit()
.frame(width: 40.0)
}
.padding()
.padding(.bottom, -20)
.cornerRadius(10)
.shadow(radius: /*#START_MENU_TOKEN#*/10/*#END_MENU_TOKEN#*/)
VStack{
Button(action: {
pins = fetchPins(predicate: "isArtwork == false")
}) {
Image("map_filter")
.resizable()
.scaledToFit()
.frame(width: 40.0)
}
.cornerRadius(10)
.shadow(radius: /*#START_MENU_TOKEN#*/10/*#END_MENU_TOKEN#*/)
.padding()
.onAppear {
pins = fetchPins(predicate: nil)
}
}
}
.padding(.bottom, 50)
}
}
}
}
extension MapView {
func fetchPins(predicate: String?) -> [Pin]{
do {
let request = Pin.fetchRequest() as NSFetchRequest<Pin>
if predicate != nil && predicate!.count > 0 {
let predicate = NSPredicate(format: predicate!)
request.predicate = predicate
}
return try moc.fetch(request)
} catch {
fatalError("Error fetching pin + predicate")
}
}
}
I think the issue is that whenever the region changes, the view gets rebuilt, so SwiftUI ends up constantly refreshing the list of annotations.
The other answer gets rid of the lag because we no longer rebuild the view when the region changes, but I noticed that when the list of annotations updates, the map region gets reset to the initial value (which makes sense, as we're using .constant(region)). To circumvent this, I did the following.
First, create a wrapper class for the region with a custom binding.
class RegionWrapper {
var _region: MKCoordinateRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 30, longitude: -90),
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
var region: Binding<MKCoordinateRegion> {
Binding(
get: { self._region },
set: { self._region = $0 }
)
}
}
Now, in the view struct, create an instance of RegionWrapper and pass regionWrapper.region to the map.
This works for most cases, but if you want to change the region programmatically, you have to make a few additional changes so that the view gets rebuilt.
First, make RegionWrapper an ObservableObject, and add a #Published flag variable. The final class should look something like this:
class RegionWrapper: ObservableObject {
var _region: MKCoordinateRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 30, longitude: -90),
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
var region: Binding<MKCoordinateRegion> {
Binding(
get: { self._region },
set: { self._region = $0 }
)
}
#Published var flag = false
}
Update the variable in the view struct to be a #StateObject. Now, whenever you want to change the region, first set regionWrapper.region.wrappedValue, and then call regionWrapper.flag.toggle() to force the view to rebuild (you can wrap these in withAnimation for a smoother transition). Example view is below.
struct MapView: View {
#StateObject private var regionWrapper = RegionWrapper()
var body: some View {
Map(coordinateRegion: regionWrapper.region, annotationItems: annotations) { ... }
}
func updateRegion(newRegion: MKCoordinateRegion) {
withAnimation {
regionWrapper.region.wrappedValue = newRegion
regionWrapper.flag.toggle()
}
}
}
Hope this helps anyone who runs into this.
I had a similar issue to this and found that changing the region
from
coordinateRegion: $region
to
coordinateRegion: .constant(region)
stopped the lag, I could still change the region from my location manager wi

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.