Hello, dear iOS people.
I hope you are all doing well.
Personal thanks to Mr. Hollemans for such a wonderful and bright book .
This question has been asked a couple of times, and after some scrutiny I had to do this .
I could optimize the code in MapViewController.swift file. But hereās the nagging problem:
- I tag a new location.
- Then go to Map and see everything works perfectly.
- Until I press the Disclosure button and it sends me to a different location object.
Mr. Hollemans, if you are reading this, can you please provide a link with the answer for this.
Thanks to everyone.
Islombek.
That sounds like the wrong Location object gets associated with the wrong map pin somehow. Try printing out the Location object for each map annotation.
Sir, I indeed did that. I put this code on the showLocationDetails()
func showLocationDetails(_ sender: UIButton) {
performSegue(withIdentifier: "EditLocation", sender: sender)
print("the button tag is" + "\(sender.tag)")
}
Hereās what happens. I deleted the app from my 6s and reinstalled it. Tagged a new location. I could see it on the Map. Everythingās fine. Then I try to go to the details by pushing the Disclosure button. Right when I do that it crashes.
Then I quit the app, go to the map straight and push the button. I thought it would crash, but no it worked. Apparently, this happens to the newly tagged objects.
When it crashes, what does Xcode look like? Is there an error message in the debug output pane?
It says
fatal error: Index out of range
When I initially tag a location and then go the map, it doesnāt show me any locations. Then I manually find the location I added and press the button, after which it crashes and says index out of range
.
Also it says that when i add a tag, then another one. When i tap on the buttons they work ok. Then i delete the first location from the list and then go to map and tap on the remaining (the second) locationās button and again it crashes saying index out of range
.
OK. UPDATE.
When a new tag is added or the existing one is updated, itās setting respective locationās button tag to its according index. Sooo, what I deduced here is that OTHER locationsā buttons are not updated unlike in updateLocations()
which deletes all the old locations and sets the new ones.
Any idea?
index out of range
means youāre trying to access an array at an index that does not exist in the array. Xcode should point at the line thatās causing the crash. Which line is that?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EditLocation" {
let navigationController = segue.destination as! UINavigationController
let controller = navigationController.topViewController as! LocationDetailsViewController
controller.managedObjectContext = managedObjectContext
let button = sender as! UIButton
let location = locations[button.tag]
controller.locationToEdit = location
}
}
let location = locations[button.tag] ā this one
OK, and what happens when you do print(locations)
or print(locations.count)
just before that line?
If that prints an empty array or 0, then it appears that youāre not doing the fetch to load the locations into this array.
However, if it doesnāt print an empty array or 0, then maybe button.tag has the wrong value. So also do print(button.tag)
to see what that is.
Sir, I did as told. Apparently, i wasnāt fetching the new array.
I tried refetching everything by putting the below-mentioned code just below if self.isViewLoaded {
let entity = Location.entity()
let fetchRequest = NSFetchRequest<Location>()
fetchRequest.entity = entity
self.locations = try! self.managedObjectContext.fetch(fetchRequest)
After that I checked the debug pane to see the print
s we put. Apparently, some buttons get the wrong tag.
So, now the code updates well for newly tagged/updated locations, but the old ones button tags arenāt updated.
The full code is this, sir.
var managedObjectContext: NSManagedObjectContext! {
didSet {
NotificationCenter.default.addObserver(forName: Notification.Name.NSManagedObjectContextObjectsDidChange, object: managedObjectContext, queue: OperationQueue.main) { notification in
if self.isViewLoaded {
// self.updateLocations()
let entity = Location.entity()
let fetchRequest = NSFetchRequest<Location>()
fetchRequest.entity = entity
self.locations = try! self.managedObjectContext.fetch(fetchRequest)
if let dictionary = notification.userInfo {
if let locations = dictionary["inserted"] as? Set<Location> {
for location in locations {
self.mapView.addAnnotation(location)
}
}
if let locations = dictionary["deleted"] as? Set<Location> {
for location in locations {
self.mapView.removeAnnotation(location)
}
}
if let locations = dictionary["updated"] as? Set<Location> {
for location in locations {
self.mapView.removeAnnotation(location)
self.mapView.addAnnotation(location)
}
}
}
}
}
}
}
and it should update the buttons too. The question is how we would do it. If we delete them all and refetch them it would just be the same thing updateLocations()
would be doing. I just have no idea on how to refetch all locations with new tags.
Have you tried getting the annotation view for the existing location and updating its tag, rather than removing the old annotation and creating a completely new one?
I tried somethings, but couldnāt manage to update the tags properly. What would you advice me to do, sir? Iām sorry for taking your time. I really appreciate your effort helping out.
Every Location is an MKAnnotation. For the Locations that are in the āupdatedā key in the dictionary, you can ask the MKMapView for their corresponding MKAnnotationView (with view(for:)
. And then you can update the tag using that MKAnnotationView.
Ok, I did try that now. In Vain.
var managedObjectContext: NSManagedObjectContext! {
didSet {
NotificationCenter.default.addObserver(forName: Notification.Name.NSManagedObjectContextObjectsDidChange, object: managedObjectContext, queue: OperationQueue.main) { notification in
if self.isViewLoaded {
// self.updateLocations()
if let dictionary = notification.userInfo {
if let locations = dictionary["inserted"] as? Set<Location> {
for location in locations {
self.mapView.addAnnotation(location)
}
}
if let locations = dictionary["deleted"] as? Set<Location> {
for location in locations {
self.mapView.removeAnnotation(location)
}
}
if let locations = dictionary["updated"] as? Set<Location> {
for location in locations {
self.mapView.removeAnnotation(location)
self.mapView.addAnnotation(location)
}
}
let entity = Location.entity()
let fetchRequest = NSFetchRequest<Location>()
fetchRequest.entity = entity
self.locations = try! self.managedObjectContext.fetch(fetchRequest)
let identifier = "Location"
let annotationView = self.mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if let annotationView = annotationView {
for location in self.locations {
let pinView = self.mapView(self.mapView, viewFor: location)
let button = annotationView.rightCalloutAccessoryView as! UIButton
if let index = self.locations.index(of: pinView?.annotation as! Location) {
button.tag = index
print("hello")
}
}
}
self.showLocations()
}
}
}
}
}
i tried putting this code inside the each if let locations
, with the same result.
I looked at this, because I thought I had it working well. But you are right, there is a problem.
The button tags all get set to the index in locations when it gets set up.
If you delete a location, the locations that come after that one all now have a lower index (they drop by 1).
If you tap a location on the map that comes after the deleted one, the button will now have the wrong index, and bring up the wrong location. If you tap the the last location in the list on the map, the button will have an invalid index (1 higher than the end of the list), and the app will crash.
Iām not sure how to fix this. You need to remove from the map all the locations that come after the deleted one, and then add them again. Or at least go through and reset all their button tags. At that point we might as well just call updateLocations and redo them all.
That creates a new annotation view. You donāt need to do that.