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 = []
}
}