RaiseMan with Printing: Swift 4.2 and Xcode 10.1


#1

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.
}

}