Why does SwiftUI Map modifies state if it has .ignoresSafeArea() modifier? - swift

import SwiftUI
import MapKit
import LocalAuthentication
struct ContentView: View {
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.4, longitude: -0.1), span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))
var body: some View {
Map(coordinateRegion: $mapRegion)
.ignoresSafeArea()
}
}
When I run this code Xcode gives this Issue:
Modifying state during view update, this will cause undefined behavior.
When I remove .ignoresSafeArea() modifier from Map() the problem disappears. Is this a bug?

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

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.

Map is unresolved identifier

I'm following the SwiftUI tutorial.
When i got to the part with Map from MapKit XCode shows the use of unresolved identifier 'Map'.
import SwiftUI
import MapKit
struct MapView: View {
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868),
span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
)
var body: some View {
Map(coordinateRegion: $region)
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}