Gold Challenge - My Solution

Hello. The way I went about solving the Gold Challenge in this chapter was by making the allItems property in the ItemStore class a type property, like so:

static var allItems = [Item]()

What this does is basically make the allitems array of items a single instance shared among the many instances of ItemStore, so there’s only one array of items to worry about. Once you make the change above, you’d need to access the allItems array through the type itself, so typing ItemStore.allItems is required anywhere this type property is used.

I then registered the ItemViewController -in its initializer- as an observer for a notification called updateNow for any posting object, as follows:

let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self,
                                       selector: #selector(updateAll),
                                       name: Notification.Name(rawValue: "updateNow"),
                                       object: nil)

I also created a notification object as a property in the ItemsViewController class:

let notification = Notification(name: Notification.Name(rawValue: "updateNow"))

next I implemented the method that should be called on the observer when the updateNow notification is posted, and exposed this method to objective-c:

@objc func updateAll() {
        tableView.reloadData()
    }

and finally proceeded to post the notification created above, every time an item was added, deleted, or moved:

@IBAction func addNewItem(_ sender: UIBarButtonItem) {
       ...      
            // Insert this new row into the table
            tableView.insertRows(at: [indexPath], with: .automatic)
        }

        NotificationCenter.default.post(notification)
    }

and here:

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        ...  
            // Also remove that row from the table view with an animation
            tableView.deleteRows(at: [indexPath], with: .automatic)

            NotificationCenter.default.post(notification)
        }
    }

and finally here:

override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        //Update the model
        itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)

        NotificationCenter.default.post(notification)
    }

With those changes, running multiple instances of LootLogger will keep them all updated with the latest changes introduced in any of the running instances, without needing to get any of the instances to the foreground active state.

If anyone has any corrections/comments on what could’ve been done better, please feel free to share!

Thanks for the tip. The way I solved this is make the itemStore static:

static var itemStore: ItemStore!

Then, I added this property in the ItemStore class:

 let notification = Notification(name: Notification.Name(rawValue: "itemsChanged"))

I made the itemStore post the notification when it went to the background and saved the data. That is, I placed the code below at the end of the “do” section of the saveChanges() function:

NotificationCenter.default.post(notification)

Finally, I added this code at the end of the modified initializer:

let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(reTable), name: Notification.Name(rawValue: "itemsChanged"), object: nil)

Please do not create a new initializer. You have already modified an initializer:

required init?(coder aDecoder: NSCoder) {...}

Remember that? That is where I inserted the code.
I am not sure if this is the best solution, but seems to work. I would love to hear your thoughts and opinions.
Thank you

I use these code

private static var realItems = [Item]()
var allItems: [Item] {
    get {
        return ItemStore.realItems
    }
    
    set(newAllItems) {
        ItemStore.realItems = newAllItems
    }
}

instead of static var allItems = [Item]()
No other associated code need to change.

1 Like

Hi, I’m seeing 2 issues. I think you have double refreshes on the first scene, and you’re also losing the table animations for the second scene by always calling .reloadData. Here’s the changes I would suggest:

  1. Remove .insertRows, .deleteRows from your existing callbacks and move them into 3 separate functions on the VC (insert/move/delete). This will get rid of double refreshes.
  2. Don’t let the VC post the notifications, but the ItemStore. Move those .post lines there. This way it doesn’t matter which scene first caused the model change.
  3. Have 3 separate notifications. The VC should observe those 3 and call the functions I mentioned in step 1. This way you don’t have to call tableView.reloadData every time which caused you to lose animations.
  4. Delete the updateAll function on the VC.
  5. Send userInfo with the notifications. The userInfo will be the indices of the affected rows.
  6. The 3 functions from number one should have the notification as a parameter, and get the row from notification.userInfo.

Example:

// In the ItemStore, createItem sends notification with row as userInfo
let notification = Notification(
  name: Notification.Name(rawValue: "insert"),
  object: nil,
  userInfo: ["at": allItems.count-1]
)
NotificationCenter.default.post(notification)
// In the ViewController, viewDidLoad observes insert notification and calls insertRow
NotificationCenter.default.addObserver(
  self,
  selector: #selector(insertRow),
  name: Notification.Name(rawValue: "insert"),
  object: nil
)
// In the ViewController, insertRow gets the row from the userInfo and animates the change
@objc func insertRow(_ notification: Notification) {
        guard let row = notification.userInfo?["at"] as? Int else {
            preconditionFailure("Invalid insert row value")
        }
        let indexPath = IndexPath(row: row, section: 0)
        tableView.insertRows(at: [indexPath], with: .automatic)
    }

There was actually a WWDC talk in 2019 which helped me understand this separation. They make it even nicer by putting a custom type into the notification.object parameter instead of just using notification.userInfo. It’s here, starting at the 10 minute mark.

I wouldn’t call addObserver in the initializer of the VC. This is UI code. tableView.reloadData() would cause a null pointer exception. Better to put addObserver into viewDidLoad.