So attached is the code from the book updated for Swift 4.2 and Xcode 10.1 that worked for me. The code is the latest version of RaiseMan that includes printing capability (that is, RaiseMan code up to Chptr 27.). Also, you need to check the checkbox for “Printing”, under Sandbox Settings, as indicated by others in this topic (see “Trouble getting the book’s code to work using Swift 4.1”). Hope it helps.
//
// AppDelegate.swift
// RaiseMan
//
// Created by Juan Pablo Claude on 2/16/15.
// Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.
//
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate
{
func applicationDidFinishLaunching(_ aNotification: Notification)
{
// Insert code here to initialize your application
// print("applicationDidFinishLaunching")
}
func applicationWillTerminate(_ aNotification: Notification)
{
// Insert code here to tear down your application
}
}
//
// Document.swift
// RaiseMan
//
// Created by Juan Pablo Claude on 2/16/15.
// Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.
// NB: FOR FILE LOADING AND SAVING, YOU NEED TO CHANGE APP SANDBOX SETTINGS.
// ... Under Project > Target > Capabilities > App Sandbox > File Access > User Selected File > "Read/Write"
//
// ALSO, FOR PRINTING: check the box for "Printing", under Sandbox Settings.
import Cocoa
// Add: for doing Undo for Edits:
private var KVOContext: Int = 0
class Document: NSDocument, NSWindowDelegate
{
// property observers for 'employees',
// so that when the array is changed the old employees will not be observed and the new ones will be:
@objc dynamic var employees: [Employee] = []
{
willSet
{
for employee in employees
{
stopObservingEmployee(employee)
}
}
didSet
{
for employee in employees
{
startObservingEmployee(employee)
}
}
}
@IBOutlet weak var tableView: NSTableView!
@IBOutlet weak var arrayController: NSArrayController!
// 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 contentArray
arrayController.addObject(employee)
// Re-sort (in case the user 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)
}
@IBAction func removeEmployees(sender: NSButton)
{
let selectedPeople: [Employee] = arrayController.selectedObjects as! [Employee]
let alert = NSAlert()
// Use NSLocalizedString:
alert.messageText = NSLocalizedString("REMOVE_MESSAGE", comment: "The remove alert's messageText")
let informativeFormat = NSLocalizedString("REMOVE_INFORMATIVE %d", comment: "The remove alert's informative Text")
alert.informativeText = String(format: informativeFormat, selectedPeople.count)
let removeButtonTitle = NSLocalizedString("REMOVE_DO", comment: "The remove alert's remove button")
alert.addButton(withTitle: removeButtonTitle)
let removeCancelTitle = NSLocalizedString("REMOVE_CANCEL", comment: "The remove alert's cancel button")
alert.addButton(withTitle: removeCancelTitle)
let window = sender.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:
// The array cotroller will delete the selected objects
// The argument to remove() is ignored
self.arrayController.remove(nil)
default:
break
}
})
}
// MARK: Accessors
// for Swift 4, you need to add @objc in front of the below "insert..." and "remove..." methods
// also, prepare(withInvocationTarget:) needs to be cast as 'AnyObject', for the undoManager to work properly
// corrections to the argument naming and parameters have been made, so as to comply with Swift 4 conventions
@objc func insertObject(_ employee: Employee, inEmployeesAtIndex index: Int)
{
Swift.print("adding \(employee) to the employees array") // since method is converted to ObjC, need to use 'Swift.print', instead of 'print'
// 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.
}
override class var autosavesInPlace: Bool
{
return true
}
override var windowNibName: NSNib.Name?
{
// 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 NSNib.Name("Document")
}
// MARK: - NSWindowDelegate
func windowWillClose(_ notification: Notification)
{
employees = []
}
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.
// NB: FOR FILE LOADING/SAVING, YOU NEED TO CHANGE APP SANDBOX SETTINGS.
// ... Under Project > Target > Capabilities > App Sandbox > File Access > User Selected File > "Read/Write"
// End editing
tableView.window?.endEditing(for: nil)
// Create an NSData object from the employees array
// use: return try NSKeyedArchiver.archivedData(withRootObject: employees, requiringSecureCoding: false)
// ...Or instead use below, that includes error handling:
guard let archive: Data = try? NSKeyedArchiver.archivedData(withRootObject: employees, requiringSecureCoding: false)
else
{
let outError:NSError! = NSError(domain: "Migrator", code: 0, userInfo: [NSLocalizedDescriptionKey : "The file could not be written"])
throw outError
}
return archive
}
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.
// NB: FOR FILE LOADING/SAVING, YOU NEED TO CHANGE APP SANDBOX SETTINGS.
// ... Under Project > Target > Capabilities > App Sandbox > File Access > User Selected File > "Read/Write"
Swift.print("About to read data of type \(typeName).")
// use: employees = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [Employee]
// ...Or instead use below, that includes error handling:
guard let archive = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [Employee]?
else
{
let outError: NSError! = NSError(domain: "Migrator", code: 0, userInfo: [NSLocalizedDescriptionKey : "The file could not be read"])
throw outError
}
employees = archive
}
// MARK: - Printing
override func printOperation(withSettings printSettings: [NSPrintInfo.AttributeKey : Any]) throws -> NSPrintOperation
{
let employeesPrintingView = EmployeesPrintingView(employees: employees)
let printInfo: NSPrintInfo = self.printInfo
let printOperation = NSPrintOperation(view: employeesPrintingView, printInfo: printInfo)
return printOperation
}
}
// Document.strings (French) ... under Document.xib folder
//
/* Class = "NSButtonCell"; title = "Add Employee"; ObjectID = "4Db-PT-NQG"; */
"4Db-PT-NQG.title" = "ajouter Employé";
/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "LQd-ie-eJT"; */
"LQd-ie-eJT.title" = "Table View Cell";
/* Class = "NSButtonCell"; title = "Remove"; ObjectID = "MuC-rs-7uV"; */
"MuC-rs-7uV.title" = "Supprimer";
/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "QGt-iQ-OFp"; */
"QGt-iQ-OFp.title" = "Table View Cell";
/* Class = "NSTableColumn"; headerCell.title = "Raise"; ObjectID = "XXy-UL-dbX"; */
"XXy-UL-dbX.headerCell.title" = "Augmentation";
/* Class = "NSTableColumn"; headerCell.title = "Name"; ObjectID = "cXq-F4-BXG"; */
"cXq-F4-BXG.headerCell.title" = "Nom";
/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "rzL-IB-Vtb"; */
"rzL-IB-Vtb.title" = "Text Cell";
/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "sEd-xC-VT3"; */
"sEd-xC-VT3.title" = "Text Cell";
/* Class = "NSWindow"; title = "Window"; ObjectID = "xOd-HO-29H"; */
"xOd-HO-29H.title" = "Window";
// Localizable.strings (Base) ... under Localizable.strings folder
//
/* The remove alert's cancel button */
"REMOVE_CANCEL" = "REMOVE_CANCEL";
/* The remove alert's remove button */
"REMOVE_DO" = "REMOVE_DO";
/* The remove alert's informative Text */
"REMOVE_INFORMATIVE %d" = "REMOVE_INFORMATIVE %d";
/* The remove alert's messageText */
"REMOVE_MESSAGE" = "REMOVE_MESSAGE";
// Localizable.string (English) ... under Localizable.strings folder
//
/* The remove alert's cancel button */
"REMOVE_CANCEL" = "Cancel";
/* The remove alert's remove button */
"REMOVE_DO" = "Remove";
/* The remove alert's informative Text */
"REMOVE_INFORMATIVE %d" = "%d people will be removed";
/* The remove alert's messageText */
"REMOVE_MESSAGE" = "Do you really want to remove these people?";
// Localizable.strings (French) ... under Localizable.strings folder
//
/* The remove alert's cancel button */
"REMOVE_CANCEL" = "Annuler";
/* The remove alert's remove button */
"REMOVE_DO" = "Supprimer";
/* The remove alert's informative Text */
"REMOVE_INFORMATIVE %d" = "%d personnes seront supprimées.";
/* The remove alert's messageText */
"REMOVE_MESSAGE" = "Voulez-vous supprimer ces personnes?";
//
// Employee.swift
// RaiseMan
//
// Created by Juan Pablo Claude on 2/16/15.
// Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18
import Foundation
class Employee: NSObject, NSCoding
{
@objc dynamic var name: String? = "New Employee"
@objc dynamic var raise: NSNumber? = 0.05 // Use NSNumber to interact with ObjC KVC-KVO and Cocoa Bindings.
// Also, needs to be optional to test for nil.
// MARK: - NSCoding
func encode(with aCoder: NSCoder)
{
if let name = name { aCoder.encode(name, forKey: "name") }
if let raise = raise { aCoder.encode(raise, forKey: "raise") }
}
required init?(coder aDecoder: NSCoder)
{
name = aDecoder.decodeObject(forKey: "name") as! String?
raise = aDecoder.decodeObject(forKey: "raise") as! NSNumber?
super.init()
}
override init()
{
super.init()
}
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)
}
}
}
//
// EmployeesPrintingView.swift
// RaiseMan
//
// Created by Juan Pablo Claude on 2/16/15.
// Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank 12/02/18.
import Cocoa
private let font: NSFont = NSFont.userFixedPitchFont(ofSize: 12.0)!
private let textAttributes: [NSAttributedString.Key : AnyObject] = [NSAttributedString.Key.font : font]
private let lineHeight: CGFloat = font.capHeight * 2.0
class EmployeesPrintingView: NSView
{
let employees: [Employee]
var pageRect = NSRect()
var linesPerPage: Int = 0
var currentPage: Int = 0
// MARK: - Lifecycle
init(employees: [Employee])
{
self.employees = employees
super.init(frame: NSRect())
}
required init?(coder decoder: NSCoder)
{
fatalError("EmployeesPrintingView cannot be instantiated from a nib.")
}
// MARK: - Pagination
override func knowsPageRange(_ range: NSRangePointer) -> Bool
{
let printOperation = NSPrintOperation.current!
let printInfo: NSPrintInfo = printOperation.printInfo
// Where can I draw?
pageRect = printInfo.imageablePageBounds
let newFrame = NSRect(origin: CGPoint(), size: printInfo.paperSize)
frame = newFrame
// How many lines per page?
linesPerPage = Int(pageRect.height / lineHeight)
// Construct the range to return
var rangeOut = NSRange(location: 0, length: 0)
// Pages are 1-based. That is, the first page is page 1.
rangeOut.location = 1
// How many pages will it take?
rangeOut.length = employees.count / linesPerPage
if employees.count % linesPerPage > 0
{
rangeOut.length += 1
}
// Return the newly constructed range, rangeOut via the range pointer
range.pointee = rangeOut
return true
}
override func rectForPage(_ page: Int) -> NSRect
{
// Note the current page
// Although Cocoa uses 1=based indexing for the page number
// it's easier not to do that here
// Return the same page rect every time return pageRect
return pageRect
}
// MARK: - Drawing
// The origin of the view is at the upper-left corner
override var isFlipped: Bool
{
return true
}
override func draw(_ dirtyRect: NSRect)
{
var nameRect = NSRect(x: pageRect.minX, y: 0, width: 200.0, height: lineHeight)
var raiseRect = NSRect(x: nameRect.maxX, y: 0, width: 100.0, height: lineHeight)
for indexOnPage in 0..<linesPerPage
{
let indexInEmployees = currentPage * linesPerPage + indexOnPage
if indexInEmployees >= employees.count
{
break
}
let employee = employees[indexInEmployees]
// Draw index and name
nameRect.origin.y = pageRect.minY + CGFloat(indexOnPage) * lineHeight
let employeeName = (employee.name ?? " ")
let indexAndName = "\(indexInEmployees) \(employeeName)"
indexAndName.draw(in: nameRect, withAttributes: textAttributes)
// Draw raise
raiseRect.origin.y = nameRect.minY
let raise = String(format: "%4.1f%%", employee.raise as! Double * 100) // Cast the NSNumber .raise as Double instead of Int
// ...Swift can't cast an NSNumber as an Int
let raiseString = raise
raiseString.draw(in: raiseRect, withAttributes: textAttributes)
}
super.draw(dirtyRect)
// Drawing code here.
}
}