V5,My Locations ,Exercise page211

Hi there, I couldn’t solve the exercise I tried to cast dictionary values to Location but it gave me this error

Could not cast value of type ‘__NSCFSet’ (0x10eb7cd98) to ‘MyLocations.Location’ (0x10cc202d0).
2018-03-25 16:57:44.815777+0430 MyLocations[3170:383284] Could not cast value of type ‘__NSCFSet’ (0x10eb7cd98) to ‘MyLocations.Location’ (0x10cc202d0).

Just giving the error message is not usually helpful since I can tell you that you are trying to cast a set to a location but unless you know how to fix the code yourself, that is not going to help you :slight_smile: Either provide the code which is causing the error or provide a ZIP archive of your project and somebody can take a look and help you.

@fahim @hollance MyLocations.zip (142.8 KB)

Do note what it says in the book about the inserted, deleted, updated objects - “This will print out an (optional) array of Location objects or nil if there were no changes.”

The value for each key is an array - in your case, an array of Location objects. So you cannot cast that array to a single Location object directly. That is why you get the crash. In your particular case you might always have one item in the array - but you still have to loop through the array and get each item, or get the first item from the array if you are confident that there’ll only be one item in the array for your particular code.

Hope that makes sense?

I try that but it doesn’t make sense because we can add array of Location directly with
mapView.addAnnotations([MKAnnotation])
and we don’t need to loop through array of Location and add it one by one

I don’t know what you are trying to do since you were assigning an array of Location objects to a variable which is simply of type Location. And when I point this out, you mention something totally unrelated about mapView.addAnnotations :slight_smile: Either I don’t understand what you want, or you might need to go back and look at your code …

1 Like

I’m getting the “Could not cast value of type ‘__NSCFSet’… error as well.
I have tried casting the content of dictionary[”(state)"] into either Location or [Location] and both do not work.
I get where OP milad is coming from, because theoretically it does not matter if we obtain a Location or an array of Location. I think because we can use mapView.addAnnotation(location) if the object is a single Location, or we can use mapView.addAnnotations(locations) - note the ‘s’ - if the object is an array of Location.
My code is as below.

var managedObjectContext: NSManagedObjectContext! {
        didSet{
            NotificationCenter.default.addObserver(forName: Notification.Name.NSManagedObjectContextObjectsDidChange, object: managedObjectContext, queue: OperationQueue.main) { notification in
                if let dictionary = notification.userInfo {
                    if self.isViewLoaded {
                        if dictionary["inserted"] != nil {
                            let locationToProcess = dictionary["inserted"] as! [Location]
                            self.mapView.addAnnotations(locationToProcess)
                        }
                        if dictionary["deleted"] != nil {
                            let locationToProcess = dictionary["deleted"] as! [Location]
                            self.mapView.removeAnnotations(locationToProcess)
                        }
                        if dictionary["updated"] != nil {
                            let locationToProcess = dictionary["updated"] as! [Location]
                            self.mapView.removeAnnotations(locationToProcess)
                            self.mapView.addAnnotations(locationToProcess)
                        }
                    }
                }
            }Preformatted text

The issue with this type of error report is that without knowing which line the crash occurs on, it is hard to know what is going on :slight_smile: I know you provided your code, but does the crash occur in there? Or does it happen elsewhere?

With the previous issue, I did take a look at @milad’s code and I believe I found the issue and told him what the issue was. In your case, if you can provide a ZIP of your project, I can do the same. Alternatively, you can try the supplied final project for the book and see if that crashes as well. If not, then you can compare your code to the final project code to see what might be going wrong.

After looking at some of the older answers for this exercise I found the problem.
The object inside the dictionary[] is not an Array but a Set.
So instead of

let locationToProcess = dictionary["inserted"] as! [Location]
                            self.mapView.addAnnotations(locationToProcess)

Cast object to Set before casting the Set to an Array

let set = dictionary["inserted"] as! Set<Location>
let locationToProcess = Array(set)
self.mapView.addAnnotations(locationToProcess)
1 Like

Thank you for this!
I had a controlled crash: Thread1: SIGABRT

LLDeBugger output:

Could not cast value of type ‘__NSCFSet’ (0x104245e78) to ‘NSArray’ (0x104246008).
2018-06-22 16:30:19.855458-0400 MyLocations[6132:151071] Could not cast value of type ‘__NSCFSet’ (0x104245e78) to ‘NSArray’ (0x104246008).
(lldb)

Source of compile error:

if userInfo["inserted"] != nil {
      let locations = userInfo["inserted"] as! [Location] // <- Breakpoint
      self.mapView.addAnnotations(locations)
    }

My solution attempt

My solution was to overload updateLocations() with updateLocations(from:) - a new one that passes the Notification.userInfo dictionary.

For starters I added a description property to Location+CoreDataProperties.swift to make the printout reader friendly:

Location+CoreDataProperties :: var description: String
extension Location {
  ...
  
  override public var description: String {
    return
"""
      
  Entity: \(locationDescription), of category: \(category), at \(date).
  Coordinate(latitude: \(latitude), longitude: \(longitude))
"""
  }

In MapViewController.swift
Within managedObjectContext’s property observer:
I assigned note.userInfo to the constant dictionary, and if self.isViewLoaded is true, dictionary is passed it into self.updateLocations(from:)

var managedObjectContext: NSManagedObjectContext!
var managedObjectContext: NSManagedObjectContext! {
  didSet { // property observer
    NotificationCenter.default.addObserver(
      forName: Notification.Name.NSManagedObjectContextObjectsDidChange,
      object: managedObjectContext,
      queue: OperationQueue.main) { note in
        /// 697 Exercise
        if let dictionary = note.userInfo {
          if self.isViewLoaded {
            self.updateLocations(from: dictionary) //  takes an argument
          }
        }
    }
  }
}

I overloaded func updateLocations() to take dictionary type [AnyHashable : Any] as an argument.
func updateLocations(from userInfo: [AnyHashable : Any])
Within it I created a closure to do the work that was identical for getting the values from all 3 keys, so the keys were passed into the closure, so the logic adjusts.

Initial code :: updateLocations(from:)
func updateLocations(from userInfo: [AnyHashable : Any]) {
  // Selected code from updateLocations(), copied here for persistence
  let entity = Location.entity()
  
  let fetchRequest = NSFetchRequest<Location>()
  fetchRequest.entity = entity
  
  locations = try! managedObjectContext.fetch(fetchRequest)
  
  /// Exercise: Only insert or delete the items that have changed.
  
  let location: (String) -> [Location] = {
    let locations = userInfo[$0] as! Set<Location>
    print(" ! location \($0): \(locations)") // Prints Entity details
    return Array(locations) }
  
  let states = ["inserted", "deleted", "updated"]
  states.forEach { state in
    if userInfo[state] != nil {
      switch state {
      case "inserted": self.mapView.addAnnotations(location("inserted"))
      case "deleted": self.mapView.removeAnnotations(location("deleted"))
      case "updated": self.mapView.removeAnnotations(location("updated"))
                      self.mapView.addAnnotations(location("updated"))
      default: return
      }
    }
  }
}

I didn’t like the string repetition, and a default in the switch made no sense since the 3 options were exhaustive… so I made an enum State, and modified my closure to take the State type, and my switch statement looked cleaner. :blush::+1:

Final Version :: updateLocations(from:)

func updateLocations(from userInfo: [AnyHashable : Any]) {
  // Selected code from updateLocations(), copied here for persistence
  let entity = Location.entity()
  
  let fetchRequest = NSFetchRequest<Location>()
  fetchRequest.entity = entity
  
  locations = try! managedObjectContext.fetch(fetchRequest)

/// Exercise: Only insert or delete the items that have changed.
enum State: String {
  case inserted, deleted, updated
}

let location: (State) -> [Location] = {
  let locations = userInfo[$0.rawValue] as! Set<Location>
  print(" ! location \($0): \(locations)") // Prints Entity details
  return Array(locations) }

let states: [State] = [.inserted, .deleted, .updated]

states.forEach { state in
  if userInfo[state.rawValue] != nil {
    switch state {
    case .inserted : self.mapView.addAnnotations(location(.inserted))
    case .deleted : self.mapView.removeAnnotations(location(.deleted))
    case .updated: self.mapView.removeAnnotations(location(.updated))
    self.mapView.addAnnotations(location(.updated))
    }
  }
}

I’m satisfied with the results.
If anyone spots any errors, or areas where I can improve, please let me know!
BTW, This took almost 4 HOURS to accomplish! Please tell me this gets easier, oh my goodness…

Cheers!
Rebecca

This got me thinking that the answer should be much simpler than my previous attempt. I reread the chapter focusing specifically on var locations, updateLocations(), var managedObjectContext, and a bit on prepare(for:sender:) to understand how managedObjectContext & locations work together, and how updateLocations() ties them together.

Second Solution Attempt

var managedObjectContext: NSManagedObjectContext! {
  didSet { // Property Observer
    NotificationCenter.default.addObserver(
      forName: Notification.Name.NSManagedObjectContextObjectsDidChange,
       object: managedObjectContext,
        queue: OperationQueue.main) { note in
        if let userInfo = note.userInfo {
          if self.isViewLoaded {
            /// Page 679 Exercise: Only insert or delete the items that have changed.
            let entity = Location.entity()
            let fetchRequest = NSFetchRequest<Location>()
            fetchRequest.entity = entity
            self.locations = try! self.managedObjectContext.fetch(fetchRequest)
            
            enum State: String { case inserted, deleted, updated }
            let states: [State] = [.inserted, .deleted, .updated]
            
            for location in self.locations {
              states.forEach { state in
                if userInfo[state.rawValue] != nil {
                  switch state {
                  case .inserted: self.mapView.addAnnotation(location)
                  case .deleted: self.mapView.removeAnnotation(location)
                  case .updated: self.mapView.removeAnnotation(location)
                                 self.mapView.addAnnotation(location)
                  }
                }
              }
            }
          }
        }
    }
  }
}

This is much simpler!
I did away with extracting the sequence values from the dictionary (which was giving everyone their errors), it wasn’t a necessary step.
Instead I looped through the location objects, and looped through each state within each location object. If one had a state that was not nil, update the mapView accordingly.

Cheers!
Rebecca

EDIT: Memory Comparison

I compared this code’s memory usage (avg 175MB) to the original code found in the book(avg 195MB), and the difference was 20MB! So cool!

I don’t think this code is doing that:

       for location in self.locations {
          states.forEach { state in
            if userInfo[state.rawValue] != nil {
  1. loop through each location - check
  2. loop through each possible state - check
  3. If one had a state that was not nil - nope

I am curious if you think that looking at userInfo[state.rawValue] is somehow looking at the state specifically for the current location - because I want to know how people form these misunderstandings.

It is plain to me that userInfo(state.rawValue] will not be nil if there are any locations with that state; it will be a Set of locations that have that state. It wouldn’t be specific to the location you are looking at unless you told it to be, which you haven’t done.

If this code seems to work, it is because it is getting just one change at a time. As I see it, if there is one insert, it will insert all of the locations again. If there is one deletion, it will delete all of the locations. What happens when you run it with 3 locations in the list, and you delete one?

I actually didn’t think it would work, but since I was only testing 1 or 2 locations at a time I assumed it was the way mapView was adding/removing annotations that was making it work.

In this new code I made sure it was specific to the location, and I created a global var
var currentIndex: Int? to hold the current location index to sync the button touched to locationToEdit.

But when I edit a location on the map, it doesn’t always load the correct location to edit.

// MARK: - Navigation
extension MapViewController {
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard segue.identifier == "EditLocation" else { return }
    
    let controller = segue.destination as! LocationDetailsViewController
    controller.managedObjectContext = managedObjectContext
    
    let button = sender as! UIButton
    let index = currentIndex ?? button.tag // added nil coalescading operator
    let location = locations[index]
    controller.locationToEdit = location
  }
}

var managedObjectContext: NSManagedObjectContext! {
  didSet { // Property Observer
    NotificationCenter.default.addObserver(
      forName: Notification.Name.NSManagedObjectContextObjectsDidChange,
      object: managedObjectContext,
      queue: OperationQueue.main) { note in
        if let userInfo = note.userInfo {
          
          if self.isViewLoaded {
            /// Page 679 Exercise: Only insert or delete the items that have changed.
            let entity = Location.entity()
            let fetchRequest = NSFetchRequest<Location>()
            fetchRequest.entity = entity
            self.locations = try! self.managedObjectContext.fetch(fetchRequest)
            
            enum State: String { case inserted, deleted, updated }
            let states: [State] = [.inserted, .deleted, .updated]
            
            var inserted = [Location]()
            var deleted = [Location]()
            
            let location: (State) -> [Location] = {
              let locations = userInfo[$0.rawValue] as! Set<Location>
              return Array(locations)
            }
            
            states.forEach { state in
              if userInfo[state.rawValue] != nil {
                switch state {
                case .inserted: inserted.append(contentsOf: location(.inserted))
                case .deleted: deleted.append(contentsOf: location(.deleted))
                case .updated: inserted.append(contentsOf: location(.updated))
                               deleted.append(contentsOf: location(.updated))
                }
              }
            }
            
            self.locations.enumerated().forEach { (locationIndex, location) in
              if inserted.contains(location) {
                self.mapView.addAnnotation(location)
                self.currentIndex = locationIndex
                if let i = inserted.index(of: location) {
                  inserted.remove(at: i)
                }
                self.mapView.view(for: self.locations[self.currentIndex!])?.reloadInputViews() // refresh pin
              }
              if deleted.contains(location) {
                self.mapView.removeAnnotation(location)
                if let i = deleted.index(of: location) {
                  deleted.remove(at: i)
                }
              }
            }
          }
        }
    }
  }
}

Thank you for taking a look a my code and pointing out my errors!
Where do you suggest I change my code to make it work as it should?
Cheers!

Oh good, I’m glad you found this issue. It is worse than that, though. Figure out which location in your list is the last one, then go to the map and tap it so the annotation is showing. Now go back to the list and delete a location before the last one. Now go back to the map, where the last one is still showing, and tap it to edit. It should bust. (The list view is sorted by category and description, making it harder to test, unless you keep the same category for all, and label them A, B, C, or something).

When you delete a location from the list, the index of every location after it changes. The index of the last one goes down -1. The index stored in the annotation button tag is not only the wrong one, but is no longer a valid one. When you tap the button, you get nothing, and it breaks. (This doesn’t happen if you use the original updateLocations every time, as it just clears out all the annotations and loads them up again).


I think if you look at your code, you will realize you are using the sets that were passed in userInfo. So you might as well just use them directly. Then you don’t need to rerun the fetch.

if self.isViewLoaded, let dictionary = notification.userInfo {

      if let insertLocations = dictionary["inserted"] as? Set<Location> {
        for location in insertLocations {
          self.locations.append(location)
          self.mapView.addAnnotation(location)
          print("*** map added a location")
        }
      } 

You can just get the sets and process the locations in them. A set is a lot like a list, same syntax to loop through. You could still use your nice enum for the state.

The deleteLocations loop will be very similar, except you also need the index of the location to remove it from the locations list:

if let index = self.locations.index(of: location) {

Updated ones are interesting. What people tend to forget is that your Locations are MKAnnotations. Unless the coordinates of the location changes, there is nothing that needs to be updated. Even then, I think the map display would pick up the change. There is not much point in removing them and adding them again - they are already there.


The visible annotation view, if there is one, is different. It is a view built from the location, so it should be updated. In my opinion, it should stay selected. So you do a bit of code to refresh the selected annotation if there is one:

      for location in self.mapView.selectedAnnotations {
        if let view = self.mapView.view(for: location), view.isSelected {
          self.mapView.deselectAnnotation(location, animated: false)
          self.mapView.selectAnnotation(location, animated: false)
          print("*** map updated a selected callout")
        }
      }

selectedAnnotations is an array, even though there is only ever 0 or 1 annotation in it. So we do a for loop anyway. (Maybe iOS 27 will support multiple selected annotations?).

The refresh causes it to rebuild the view for the annotation, so it gets the correct index into the button tag. It also will display your edit if you changed the title or category - even if you edited it from the list tab, not the map tab. It’s all wired up!

1 Like

@sgerrard Steve,

Implemented your changes and the app works flawlessly! Thank you for your help, I learned so much!

This got me thinking of multiple annotations - which ultimately got me thinking about how photos are “clustered”. I’d like to attempt that once I’ve completed the Locations app, or after I’ve completed the final tutorial/book.

Searching a bit on Apple’s online documentation lead me to this: → Decluttering a Map with MapKit Annotation Clustering

A demo app is available for download TANDm on that page.
Note: It won’t build unless you modify two dictionary entries within ClusterAnnotationView.swift where the text is drawn:

let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.black,
                   NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 20)]

NSAttributedString.Key needs to be changed to NSAttributedStringKey

let attributes = [ NSAttributedStringKey.foregroundColor: UIColor.black,
                   NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 20) ]

My plan is to cluster based on Categories.

Thank you again for your help!
Rebecca

Pages 717 & 718 introduces a Closure’s ownership cycle and a closure’s capture list → [weak self]

In MapViewController.swift I added a capture list [weak self] to managedObjectContext’s addObserver(forName:object:queue:using:)

var managedObjectContext: NSManagedObjectContext! {
    didSet {
      NotificationCenter.default.addObserver(
        forName: Notification.Name.NSManagedObjectContextObjectsDidChange,
         object: managedObjectContext,
          queue: OperationQueue.main) { [weak self ] note in // capture list
          guard let weakSelf = self else { return } // weakSelf: MapViewController
          
          if let userInfo = note.userInfo, weakSelf.isViewLoaded { // weakSelf
            /// Page 679 Exercise: Only insert or delete the items that have changed.
            enum State: String { case inserted, deleted }

            ... replaced all instances of self with weakSelf ...

Question Was the capture list necessary - Is there a difference between a closure that captures a strong ref to self in this addObserver(forName:object:queue:using:) and the one in the book example in LocationDetailsViewController.swift where addObserver(forName:object:queue:using:) is called within listenForBackgroundNotification().

Question If there is a difference - what makes them different?

Question If there is no difference - do I need to create an instance var similar to LocationDetailsViewController.swift’s var observer: Any! and deinit{} it here in this ViewController as well?


Fatal Error When I do a fresh install, and never select the Map tab (mapView is nil), I get a fatal error if I create a annotation because mapView is nil.

The code is breaking: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an optional value
It appears the mapVIew is nil.
image

image

I managed to remedy this with a guard let statement.

// refresh the selected annotation (if exists)
guard let mapView = weakSelf.mapView else { return } // <- added
mapView.selectedAnnotations.forEach { location in
  if let view = mapView.view(for: location), view.isSelected {
    mapView.deselectAnnotation(location, animated: false)
    mapView.selectAnnotation(location, animated: false)
    print("*** map updated a selected callout: \(location)")
  }
}

Cheers!
Rebecca

The MapViewController is located in one of the tabs of the app. That means it will be created at startup, and stay present until the app exits. That means self will always be something. (Technically it is an owned self). The class object is created at startup, and the managedObjectContext gets set, but the view doesn’t load until you first click the tab.

The LocationDetailsViewController, on the other hand, is created every time you start an edit, and discarded every time you close the edit view (Cancel or Done). So it will not always be something. That’s why it needs to check for self, and why it is good to remove the notification in DeInit.

The code for refreshing the annotation should be inside the closure for the notification, and also inside the test at the start of it:

if let userInfo = note.userInfo, weakSelf.isViewLoaded {
// do inserts
// do deletes
// refresh annotations
}

Then it won’t get to the refresh annotations part if the view has not been loaded yet. That test at the beginning, by the way, is a good candidate for turning into a guard statement.

1 Like

This was my issue. At one point it was within it, but after so many edits, I lost track of that one bracket :wink:

Steve, that was a perfect explanation that has fundamentally changed the way I think about ViewController objects and their life. I can’t thank you enough for this. :+1: :+1: :+1:

Rebecca

This topic was automatically closed after 166 days. New replies are no longer allowed.