NSUndoManager Document.swift and Employee.swift for Swift 4.2, Xcode 10.1


#1

Employee.swift and Document.swift for Swift 4.2:

Here’s what worked for me, in Swift 4.2 (Xcode 10.1). You may need to disconnect and reconnect your IBOutlets/IBActions (as described in the NSUndoManager book chapter) so that the proper naming conventions are used for the newer Swift version. Also, you may need to reset your Swift Language Version in Xcode (under Build Settings) to Swift 4.2 and the ‘Background’ for Table View (in interface builder) should be set (in the popup menu) to “Default (Control Background Color)”, to see text clearly in the table view, especially if you’re using Dark Mode on your system. Hope it helps.

//
//  Employee.swift
//  RaiseMan
//
//  Created by Nate Chandler on 9/1/14.
//  Copyright (c) 2014 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 11/1/18

import Foundation

class Employee: NSObject {
    @objc dynamic var name: String? = "New Employee"
    @objc dynamic var raise: NSNumber? = 0.05       // You need to use NSNumber to interact with ObjC KVC-KVO.  Needs to be optional to test for nil.
    
    
    override func validateValue(_ ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey inKey: String) throws
    {
        if raise == nil
        {
            let domain = "UserInputValidationErrorDomain"
            let code = 0
            let userInfo = [NSLocalizedDescriptionKey: "Error, raise is nil"]
            
            throw NSError(domain: domain,
                          code: code,
                          userInfo: userInfo)
        }
    }
}



//
//  Document.swift
//  RaiseMan
//
//  Created by Nate Chandler on 9/1/14.
//  Copyright (c) 2014 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 11/1/18

import Cocoa

private var KVOContext: Int = 0

class Document: NSDocument, NSWindowDelegate
{
    @IBOutlet weak var tableView: NSTableView!
    @IBOutlet weak var arrayController: NSArrayController!
    @objc dynamic var employees: [Employee] = []
    {
        willSet
        {
            for employee in employees
            {
                stopObservingEmployee(employee)
            }
        }
        didSet
        {
            for employee in employees
            {
                startObservingEmployee(employee)
            }
        }
    }
    
    
    
    // MARK: - Actions
    @IBAction func addEmployee(sender: NSButton)
    {
        let windowController = windowControllers[0]
        let window = windowController.window!
        
        let endedEditing = window.makeFirstResponder(window)
        if !endedEditing
        {
            Swift.print("Unable to end editing")
            return
        }
        
        let undo: UndoManager = undoManager!
        
        // Has an edit occurred already in this event?
        if undo.groupingLevel > 0 {
            // Close the last group
            undo.endUndoGrouping()
            // Open a new group
            undo.beginUndoGrouping()
        }
        
        // Create the object
        let employee = arrayController.newObject() as! Employee
        
        // Add it to the array controller's content array
        arrayController.addObject(employee)
        
        // Re-sort (in case the use has sorted a column) 
        arrayController.rearrangeObjects()
        
        // Get the sorted array
        let sortedEmployees = arrayController.arrangedObjects as! [Employee]
        
        // Find the object just added
        let row = sortedEmployees.index(of: employee)!
        
        // Begin the edit in the first column
        Swift.print("starting edit of \(employee) in row \(row)")
        tableView.editColumn(0, row: row, with: nil, select: true)
    }
    
    
    
    // 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: UndoManager = undoManager!
        (undo.prepare(withInvocationTarget: self) as AnyObject).removeObject(fromEmployeesAtIndex: employees.count)
        if !undo.isUndoing
        {
            undo.setActionName("Add Person")
        }
        
        employees.append(employee)
    }
    
    @objc func removeObject(fromEmployeesAtIndex 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)
    }
    
    // MARK: - Key Value Observing
    
    func startObservingEmployee(_ employee: Employee)
    {
        employee.addObserver(self, forKeyPath: "name", options: .old, context: &KVOContext)
        employee.addObserver(self, forKeyPath: "raise", options: .old, context: &KVOContext)
    }
    
    func stopObservingEmployee(_ employee: Employee)
    {
        employee.removeObserver(self, forKeyPath: "name", context: &KVOContext)
        employee.removeObserver(self, forKeyPath: "raise", context: &KVOContext)
    }
    
    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: AnyObject? = change![NSKeyValueChangeKey.oldKey] as AnyObject
        if oldValue is NSNull
        {
            oldValue = nil
        }
        
        let undo: UndoManager = undoManager!
        Swift.print("oldValue = \(String(describing: oldValue))")
        
        (undo.prepare(withInvocationTarget: object!) as AnyObject).setValue(oldValue, forKeyPath: keyPath!)
    }
    
    
    // MARK - Lifecycle
    
    override init()
    {
        super.init()
        // Add your subclass-specific initialization here.
                                    
    }

    
    
    // MARK - NSDocument Overrides
    
    override func windowControllerDidLoadNib(_ aController: NSWindowController)
    {
        super.windowControllerDidLoadNib(aController)
                                    
        // Add any code here that needs to be executed once the windowController has loaded the document's window.
    }

    override class var autosavesInPlace: Bool
    {
        return true
    }

    override var windowNibName: String
    {
        // Returns the nib file name of the document
        // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this property and override -makeWindowControllers instead.
        return "Document"
    }

    override func data(ofType typeName: String) throws -> Data
    {
        // Insert code here to write your document to data of the specified type, throwing an error in case of failure.
        // Alternatively, you could remove this method and override fileWrapper(ofType:), write(to:ofType:), or write(to:ofType:for:originalContentsURL:) instead.
        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }
    
    override func read(from data: Data, ofType typeName: String) throws
    {
        // Insert code here to read your document from the given data of the specified type, throwing an error in case of failure.
        // Alternatively, you could remove this method and override read(from:ofType:) instead.
        // If you do, you should also override isEntireFileLoaded to return false if the contents are lazily loaded.
        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }

    
    
    // MARK: - NSWindowDelegate
    
    func windowWillClose(_ notification: Notification)
    {
        employees = []
    }
}