2nd Challenge question - SPOILERS


#1

I’ve implemented the following solution:

[code]
// MARK: - menu

override func validateMenuItem(menuItem: NSMenuItem) -> Bool {
    switch menuItem.action {
    case Selector("delete:"):
        return arrayController.selectedObjects.count > 0
    default:
        return super.validateMenuItem(menuItem)
    }
}

@IBAction func delete(sender: AnyObject?) {
    removeEmployees(self.removeButton)
}[/code]

I’m using the array controller selected item count to ensure the delete menu item is only available when items are selected.

I also made a reference to the remove button to pass into the removeEmployees method. I did this to avoid changing the code too much in case this class is used later in the book.

So this code works, but … unfortunately when it first launches, the delete menu item is enabled, even though no employees are selected.

I have two questions:

  1. How do I fix the enabled delete menu item that should be disabled on launch?
  2. Is this a decent implementation for the challenge?

#2

I tried something similar, but after giving up, I tried your way. When I select the new menu item, I crash on the call to removeEmployees with “fatal error: unexpectedly found nil while unwrapping an Optional value” as I had with other attempts. I assume you linked the menu item to the delete: selector in First Responder, right? I don’t know how your code isn’t crashing on the same error.


#3

Are you sending the button referenced as I have? It needs this because it gets the parent window from it. I’d run through the debugger and see what’s coming back nil.


#4

I finally got back to looking at this. I figured out my bug. My connection to my remove: button outlet was broken (I chose a different one). Regarding your first question, I note that the Remove Employees button is enabled on launch, even though canRemove is bound to enabled in the Array Controller. Once you select a row and then deselect it, the button gets disabled. The same thing happens with the new remove menu item.

If there was guidance to disable Remove Employees button on launch, I didn’t see that. It’s easy enough to fix:

    // MARK - NSDocument Overrides
    
    override func windowControllerDidLoadNib(aController: NSWindowController) {
        super.windowControllerDidLoadNib(aController)
        
        removeButton.enabled = false
    }

However, no matter what I tried, I couldn’t figure out how to do that for the new menu item, at least if it’s created in Interface Builder (I didn’t try it by creating the item programmatically, but that may give more options).

Anyway, I gave it a shot. :slight_smile:


#5

I added this in the windowControllerDidLoadNib method:

weak var weakSelf: Document? = self dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.0 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in println("arrayController.selectedObjects.count: \(weakSelf!.arrayController.selectedObjects.count)") println("tableView.numberOfSelectedRows: \(weakSelf!.tableView.numberOfSelectedRows)") }

The result is:

arrayController.selectedObjects.count: 1 tableView.numberOfSelectedRows: 0

For some reason the array controller always has a selected index count of 1, while the tableView is 0. Perhaps this is happening when it unarchives objects? To correct this, I added the following line to the same method:

When I ran the code again, I get this:

arrayController.selectedObjects.count: 0 tableView.numberOfSelectedRows: 0
In addition, the delete menu item was disabled as well as the remove button.


#6

I tried what you posted and got the same results. After examining your code, I eliminated all of it except the “arrayController.setSelectionIndexes(NSIndexSet())” line and got the proper able/disable action for both the button and menu item.

Bottom line, you found the best way to do it, I think. Good job!


#7

If you change the logic for how you get the window there is no need for creating outlets to the “remove” button or a second function to call the removeEmployees(sender: NSButton) function. If you had validation for many menu items all the duplication would get cumbersome.

PS. You can also make sure the arrayController has nothing selected by setting its selection index to -1 instead of creating an empty NSIndexSet.


#8

Thanks for the tips. I thought of refactoring the window logic, but wasn’t sure if future chapters would need it, so I left it in. I changed the selection index code per your suggestion.


#9

Change your removeEmployees method’s way of getting a handle of the window by adding this at the very top

guard let windowController = windowControllers.first, window = windowController.window else { return }It wouldn’t make sense to remove anything or display an alert if there is no window.

Now you can call the same function from your new Menu Item. You might change its signature so the sender is AnyObject.

Add the validateMenuItem method like above.

Go into your Document.xib select the arrayController in the outline and uncheck Avoid Empty Selection in the Attributes Inspector.
Problem with initial state solved.
No extra code necessary.


#10

Just throwing my $0.02 in there: I solved this by adding the guard statement in removeEmployees() like @tkrajacic suggested, kept the signature as NSButton, didn’t create an extraneous delete: method (I thought that did more harm than good.

I then created a new menu item (set it’s shortcut to cmd+delete), and attached its action to removeEmployees().

To ensure that the delete function was only available when items existed and were selected in the tableview, I simply did a check to see if the numberOfSelectedRows of the tableView was > 0.

// MARK: - Menu
	
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
	if tableView.numberOfSelectedRows > 0 {
		return true
	}
	return false
}

I should mention that all of this was done on Swift 3, so your solutions might be slightly different.

I figured that was easier than monitoring whether or not a function could be called at any given moment (I’m also having trouble with Selectors in Swift 3–but that’s a different matter altogether.


#11

Did this slightly differently:

used the guard statement (from tkrajacic above), along with removing the window = sender.window! statement (as NSMenuItem has no receiver ‘window’), and added a slightly modified validMenuItem method that doesn’t require additional changes to interface builder(i.e. instead of deselecting ‘Avoid Empty Selection’…the easy way…). Everything else done as described above by macintacos (Thanks, and cheers.)

Code is below (Swift 4.2):

@IBAction func removeEmployees(_ sender: NSButton)
    {
        Swift.print("removeEmployeesWithSender")
        guard let windowController = windowControllers.first, let window = windowController.window
            else
        {
            return  // won't make sense to remove anything or display an alert if there's no window
        }
        
        
        let selectedPeople: [Employee] = arrayController.selectedObjects as! [Employee]
        let alert = NSAlert()

        alert.messageText = "Do you really want to remove these people?"
        alert.informativeText = "\(selectedPeople.count) people will be removed."
        alert.addButton(withTitle: "Keep, No Raise")
        alert.addButton(withTitle: "Remove")
        alert.addButton(withTitle: "Cancel")

//        window = sender.window!       // NSMenuItem has no receiver "window" (works for push button but not for menu item),
                                        // so need the above guard code for checking and setting the window.

        alert.beginSheetModal(for: window, completionHandler: { (response) -> Void in
            // If the user chose "Remove", tell the array controller to delete the people
            switch response
            {
                case NSApplication.ModalResponse.alertFirstButtonReturn:
                    // Keep but No Raise:
                    for employee in selectedPeople
                    {
                        employee.raise = 0.05
                    }
                case NSApplication.ModalResponse.alertSecondButtonReturn:
                    // The array cotroller will delete the selected objects
                    // The argument to remove() is ignored
                    self.arrayController.remove(nil)

                default:
                    break
            }
        })
        
    }
    

    override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool
    {   
        switch menuItem.action
        {
            case #selector(self.removeEmployees(_:)):
                return tableView.numberOfSelectedRows > 0       // if greater than zero, return = true
                                                                // validates whether a selection has been made before removing an item
            default:
                return true
        }
    }