Completing 'Adding Undo to RaiseMan' using Swift 3.0


#1

I’ve made the following (Xcode recommended) changes to the ‘Adding Undo to RaiseMan’ code to be compatible with Swift 3.0. However, the undo and redo functionality doesn’t appear to be available when running the application.

// MARK: - Accessors

func insertObject(employee: Employee, inEmployeesAtIndex index: Int) {
    print("adding \(employee) to the employees array")
    
    // Add the inverse of this operation to the undo stack
    let undo: UndoManager = undoManager!
    (undo.prepare(withInvocationTarget: self) as! Document).removeObjectFromEmployeesAtIndex(index: employees.count)
    
    if !undo.isUndoing == false {
        undo.setActionName("Add Person")
    }
    
    employees.append(employee)
}

func removeObjectFromEmployeesAtIndex(index: Int) {
    let employee = employees[index]
    
    print("removing \(employee) from the employees array")
    
    // Add the inverse of this operation to the undo stack
    let undo: UndoManager = undoManager!
    (undo.prepare(withInvocationTarget: self) as! Document).insertObject(employee: employee, inEmployeesAtIndex: index)
    
    if !undo.isUndoing == false {
        undo.setActionName("Remove Person")
    }
    
    employees.remove(at: index)
}

Has anyone else tried completing this chapter using Xcode 8.0 and Swift 3.0? I’m not really comfortable moving forward until I understand what’s going on and how to properly implement UndoManager.

Thanks in advance for any suggestions!


#2

I’m working on this too. I can get the accessor methods to be called by renaming them in accordance with Swift 3 naming conventions, as follows:

func insertObject(_ employee: Employee, inEmployeesAtIndex index: Int) {
    Swift.print("adding \(employee) to the employees array")
    // ...
}

func removeObject(fromEmployeesAtIndex index: Int) {
    Swift.print("removing employee")
    // ...
}

However, I can’t get the prepare(withInvocationTarget:) line to compile. Does it work for you?


#3

The book version:

undo.prepare(withInvocationTarget: self).removeObject(fromEmployeesAtIndex: employees.count)

fails at compile time with:

Value of type 'Any' has no member 'removeObject'

The version you use above (corrected for the new accessor name):

(undo.prepare(withInvocationTarget: self) as! Document).removeObject(fromEmployeesAtIndex: employees.count)

fails at run time with:

Could not cast value of type 'NSUndoManagerProxy' (0x7fffa669e760) to 'RaiseMan.Document' (0x1000061e0).


#4

This Stack Overflow thread suggests that prepare(withInvocationTarget:) is bugged in Swift 3 and proposes using registerUndo(withTarget:) instead.

To get the table to update, I also had to declare employees as dynamic. A complete working Swift 3 version is below:

dynamic var employees: [Employee] = []

// ...

// MARK: - Accessors

func insertObject(_ employee: Employee, inEmployeesAtIndex index: Int) {
        Swift.print("adding \(employee) to the employees array")

    // Add the inverse of this operation to the undo stack
    if let undo = undoManager {
        undo.registerUndo(withTarget: self) { target in
            target.removeObject(fromEmployeesAtIndex: index)
        }
        undo.setActionName("Add Person")
    }

    employees.append(employee)
}

func removeObject(fromEmployeesAtIndex index: Int) {
    let employee = employees[index]
    Swift.print("removing \(employee) from the employees array")

    // Add the inverse of this operation to the undo stack
    if let undo = undoManager {
        undo.registerUndo(withTarget: self) { target in
            target.insertObject(employee, inEmployeesAtIndex: index)
        }
        undo.setActionName("Remove Person")
    }

    employees.remove(at: index)
}

Any suggestions for improvement would be appreciated.


#5

Good work. Why dynamic?


#6

Note compiled, but crashed on the run


#7

adding <RaiseMan.Employee: 0x600000046870> to the employees array
Could not cast value of type ‘NSUndoManagerProxy’ (0x7fffeda07760) to ‘RaiseMan.Document’ (0x100008450).
2016-11-02 17:07:01.870621 RaiseMan[23177:2287627] Could not cast value of type ‘NSUndoManagerProxy’ (0x7fffeda07760) to ‘RaiseMan.Document’ (0x100008450).

(errors)


#8

also added observeValue(forKeyPath:) method for Swift 3:

 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    
    if context != &KVOContext {
        // if the context does not match, this message
        // must be intended for our superclass
        
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }
    
    
    var oldvalue = change?[NSKeyValueChangeKey.oldKey] as AnyObject?
    if oldvalue is NSNull {
        oldvalue = nil
    }

    
    // Add the inverse of this operation to the undo stack
    if let undo = undoManager {
        undo.registerUndo(withTarget: self) { target in
            target.setValue(oldvalue, forKeyPath: keyPath!)
        }
        undo.setActionName("Add Person")
    }
 }

#9

https://github.com/bignerdranch

go here and get the code for the book. While the book has 1.2, the code in the github is currently 2.0. If you convert it to 3.0 with XCode 8.1, you’ll see withInvocationTarget works (to my surprise) and the code runs as expected, including a tableview refresh I wasn’t able to make work. I wish the text was better about finishing up the app and getting it to work. It gets you ‘close’ but you have to go to the codebase on github to really see it working.


#10

For the last line, it is better to update it as

employees.insert(employee, at: index)


#11

Just a follow up …

  1. @gbeck was right in the beginning. Following the Swift 3 naming convention the first parameter of insertObject and the parameter of removeObject have to be succeeded by a underscore (_) otherwise the methods can’t be called without a parameter name. That’s mandatory . If not, the whole Key Value Coding mechanism just won’t work.

  2. As for the other problem raised by @gbeck (the complication of prepare(withInvocationTarget:) ) just cast the Any type that is returned by prepare(withInvocationTarget: self) to AnyObject:

(undo.prepare(withInvocationTarget: self) as AnyObject)

The following code will just run fine in Xcode 8.2

/ / MARK: - Accessors

func insertObject(_ employee: Employee, inEmployeesAtIndex index: Int) {
    Swift.print("adding \(employee) to the employees array")
    // Add the inverse of this operation to the undo stack
    
    
    let undo: UndoManager = undoManager!
    (undo.prepare(withInvocationTarget: self) as AnyObject)
        .removeObjectFromEmployeesAtIndex(employees.count)
    if !undo.isUndoing {
        undo.setActionName("Add Person")
    }
    
    employees.append(employee)
}

func removeObjectFromEmployeesAtIndex(_ index: Int) {
    let employee: Employee = employees[index]
    Swift.print("removing \(employee) from the employees array")
    // Add the inverse of this operation to the undo stack
    
    let undo: UndoManager = undoManager!
    (undo.prepare(withInvocationTarget: self) as AnyObject)
        .insertObject( employee, inEmployeesAtIndex: index)
    if !undo.isUndoing {
        undo.setActionName("Remove Person")
    }
    
    // Remove the Employee from the array
    employees.remove(at: index)
}

#12

How about for Swift 4.0? Does anyone have any suggestions on where to look for help? The 3.0 version above compiles, but the undo and redo remain grayed out and do nothing. I haven’t been able to find anything yet on how to get this to work using Xcode 9.2 with Swift 4.0.

I’m new to programming and have been through books on C++ and Objective-C, and now trying to get through this BNR book that has outdated syntax throughout (understandably since Swift has apparently gone from 2.0 to 4.0 in 3ish years).


#13

Works ok with swift 4. The one problem I was running into was that it looks like the KVO requires both methods to be defined before they will work. I was trying to get the insert method to work first, but it never did until I added the remove method as well. They are expected to be a matched set.

My methods ended up looking like this:

@objc dynamic var employees: [Employee] = []
...

@objc func insertObject(_ employee: Employee, inEmployeesAtIndex index: Int) {

    // Add the inverse of this operation to the undo stack
    let undo = self.undoManager!
    (undo.prepare(withInvocationTarget: self) as AnyObject).removeObjectFromEmployeesAtIndex(index)
    if !undo.isUndoing {
        undo.setActionName("Add Person")
    }

    employees.insert(employee, at: index)
}

@objc func removeObjectFromEmployeesAtIndex(_ index: Int) {

    // Add the inverse of this operation to the undo stack
    let employee: Employee = employees[index]
    let undo: UndoManager = undoManager!
    (undo.prepare(withInvocationTarget: self) as AnyObject).insertObject(employee, inEmployeesAtIndex: index)
    if !undo.isUndoing {
        undo.setActionName("Remove Person")
    }

    employees.remove(at: index)
}

#14

Thanks for the reply. I must have something wrong somewhere. Where you have AnyObject in your code, won’t work in mine (see edit below). I have to use ‘as! Document’, or I get the error message: Cannot invoke ‘insertObject’ with an argument list of type ‘(Employee, inEmployeesAtIndex: Int)’

It compiles but the undo and redo under the ‘Edit’ menu are still greyed out and won’t work, no matter how many times I change a name in the table. The functions aren’t being called. Breakpoints aren’t hit. Also, I have the exact same results when I remove the ‘@objc’ from each function.

** edit - I have now learned that when I have ‘@objc’ before each function, I can use ‘as AnyObject’ with no error messages. BUT, the undo and redo still aren’t working, and neither function is getting called. I must have something wrong somewhere else…back to the drawing board…again.

    @objc dynamic var employees: [Employee] = []

	...
	
	// MARK: - Accessors
	
	@objc func insertObject(_ employee: Employee, inEmployeesAtIndex index: Int) {
		Swift.print("adding \(employee) to the employees array")
		
		// Add the inverse of this operation to the undo stack
		
		let undo = self.undoManager!
		
		(undo.prepare(withInvocationTarget: self) as! Document)
			.removeObjectFromEmloyeesAtIndex(index)
		if !undo.isUndoing {
			undo.setActionName("Add Person")
		}
		
		
		employees.insert(employee, at: index)
	}
	
	//--------------------------------------------------------------------------------------------------------------
	
	@objc func removeObjectFromEmloyeesAtIndex(_ index: Int) {
		let employee: Employee = employees[index]
		Swift.print("removing \(employee) from the employees array")
		
		// Add the inverse if this operation tot he undo stack
		
		let undo: UndoManager = undoManager!
		(undo.prepare(withInvocationTarget: self) as! Document)
			.insertObject(employee, inEmployeesAtIndex: index)
		if !undo.isUndoing {
			undo.setActionName("Remove Person")
		}
		
		// Remove the Employee from the Array
		employees.remove(at: index)
		
	}

#15

It may be due to your Employees spelled wrong in your remove method name. This prevents KVC from working as specific naming is required. Also you mention it not working for name changes, but this code is for adding/removing items only.


#16

Wow. I’ve looked at this thing so many times I can’t count. I’m sorry to put you through reviewing my code to find a stupid typo that I should have found a long time ago. Thank you for your generous time. I’ll try to proofread better in the future. Works as advertised now that the typo has been corrected.

I realized I said name changes, and misspoke. I understand it is just for add/remove only.

Thanks again.

Joel


#17

Ha! No problem. I was a professional programmer for 30 years and have done that kind of thing more times than I can count. Glad that it’s working for you now.


#18

j2000lbs,

If you haven’t already, read this: Key-Value Coding Programming Guide.

Apple provides lots of useful programming guides.


#19

Thanks! As you can tell, I’ll need all the help I can get. I have been reading through the guides, but I’ll read that one now.