MyLocations V5.0: Killer Exercise on page 211

Hello, dear iOS people.
I hope you are all doing well.
Personal thanks to Mr. Hollemans for such a wonderful and bright book :slight_smile: .
This question has been asked a couple of times, and after some scrutiny I had to do this :smiley:.
I could optimize the code in MapViewController.swift file. But hereā€™s the nagging problem:

  1. I tag a new location.
  2. Then go to Map and see everything works perfectly.
  3. 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. :blush:

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

SIR!
I finally could get it! Yeeeeey! :smile::sob::hugging::blush::smile: Thank you sooooo much! :))))))) im feeling over the moon! :smiley: Well, the code is almost the same as my previous post. But the difference was made when u told to that im making a new annotation! I tried looking at my code again and noticed that im getting the view but not using. Then it hit me!

for location in self.locations {
let pinView = self.mapView(self.mapView, viewFor: location)
if let pinView = pinView {
let button = pinView.rightCalloutAccessoryView as! UIButton
if let index = self.locations.index(of: pinView.annotation as! Location) {
button.tag = index
print(ā€œhelloā€)
}
}
}

I checked it on the simulator and just checking on my 6S. Flawless. :slight_smile: you are the best, sir! Thank you!
This app is going to be the precursor to my townā€™s custom map since Google Maps is not so developed and proliferated here in Uzbekistan. Thank you, sir, once and a million times more. Wish you good luck in your job and super strong health forever. Iā€™ll hopefully have some more questions just so iā€™ll be sure im on the right course.
ā€“ Islombek.