Finalizing the Book List Application
Our application is not complete until we can save, update and delete books. In this tutorial we will add this functionality.
Final Project
Let’s get started with properties
We add a couple of properties first. The “doubleClickedSelectedRow” keeps track of the row index when a row is double clicked in the table view for editing. This way if another row is accidentally selected before the update button is clicked, the correct book will still be updated.
I have set the path to the desktop for convenience only. Change this to your liking.
property doubleClickedSelectedRow : -1
property FILE_PATH : POSIX path of ((path to desktop as string) & "BookList.plist")
AwakeFromNib
We add a double-click action to our table view to point to a handler we will create shortly. Next we either read in our saved data or load test data if no file is found. We use NSArray’s “initWithContentsOfFile_” method to read in the file if it is found. NSArray is the parent class of NSMutableArray so we have all of its methods available to us.
on awakeFromNib()
-- Set double-click action on table view
aTableView's setDoubleAction_("tableDoubleClicked")
set theDataSource to NSMutableArray's alloc()'s initWithContentsOfFile_(FILE_PATH)
if theDataSource is equal to missing value then
set theDataSource to NSMutableArray's alloc()'s init()
set theData to {{bookTitle:"A Christmas Carol", theAuthor:"Charles Dickens", theStatus:1}, {bookTitle:"The Adventures of Huckleberry Finn", theAuthor:"Mark Twain", theStatus:1}, {bookTitle:"The Adventures of Tom Sawyer", theAuthor:"Mark Twain", theStatus:0}, {bookTitle:"War and Peace", theAuthor:"Leo Tolstoy", theStatus:0}}
theDataSource's addObjectsFromArray_(theData)
end if
aTableView's reloadData()
end awakeFromNib
Double-click Action
We first check to see if the selectedRow is -1. If you double-click inside the table view in the blank space beneath the last row, the value of selectedRow will be -1.
If selectedRow is not -1, we retrieve the book from theDataSource array by its index, the value of selectedRow. To get the values from the book we use the NSDictionary’s “valueForKey_” method. This is equivalent to the AppleScript method “get theStatus of thisBook” when getting values from a record.
The next few lines are setting the value of our properties that we bound to the value of the text fields and radio button matrix. You might think that you can set these properties with the following, but you would be mistaken.
Our properties have two methods created automatically for us, a getter and a setter. We use these methods to “get” and “set” their values. Since the properties are bound to the fields, double-clicking on a table row will insert those values in their respective fields.
Example Getter and Setter Methods in Objective-C
tableDoubleClicked Handler
on tableDoubleClicked()
if aTableView's selectedRow as integer is not equal to -1 then
my setDoubleClickedSelectedRow_(aTableView's selectedRow as integer)
set thisBook to theDataSource's objectAtIndex_(doubleClickedSelectedRow as integer)
my setTheStatus_(thisBook's valueForKey_("theStatus"))
my setBookTitle_(thisBook's valueForKey_("bookTitle"))
my setTheAuthor_(thisBook's valueForKey_("theAuthor"))
end if
end tableDoubleClicked
Updating Our Book
Updating our book is very simple. We gather the information into a record the same as we do when creating a new book and then replace the book being updated with the new one.
We use NSMutableArray’s “replaceObjectAtIndex_withObject_” method to accomplish this task. Next we reload the table view and update our file using “writeToFile” which we will write next.
on updateBookInfo_(sender)
tell bookTitleField to selectText_(me) -- forces all fields to complete editing so that our properties are up to date ( thanks Shane Stanley )
if my checkFields() then
return
end if
set updatedBook to {bookTitle:bookTitle, theAuthor:theAuthor, theStatus:theStatus}
theDataSource's replaceObjectAtIndex_withObject_(doubleClickedSelectedRow as integer, updatedBook)
aTableView's reloadData()
my writeToFile()
my clearFields()
end updateBookInfo_
Saving Our Book List Data
Saving to file is very simple. We use the NSArray’s “writeToFile_atomically_” method. It returns “YES” if successful and “NO” if not. If there was an error we display a dialog.
The “writeToFile_atomically_” writes a “plist” file. As you can see below, our BookList.plist file contains an array of dictionaries holding our data structure.
At first this looks strange but it is actually very well organized and easy to read. Every book item is one dictionary inside the array and each dictionary contains key-value pairs of our data.
Click to see larger image
ImageTitle
on writeToFile()
if not theDataSource's writeToFile_atomically_(FILE_PATH, true) then
set messageText to "There was an error writing to file"
display dialog messageText buttons {"Ok"} default button 1
end if
end writeToFile
Also add these methods
on clearFields()
-- Clear fields
bookTitleField's setStringValue_("")
authorField's setStringValue_("")
-- Clear property variables
setTheAuthor_("")
setBookTitle_("")
end clearFields
on checkFields()
set missingValues to false
repeat with anItem in {theAuthor, bookTitle}
if anItem is in {"", missing value} then
set missingValues to true
display dialog "The Title and/or Author fields are empty!" with icon 0 buttons {"Ok"} default button 1
return
end if
end repeat
return missingValues
end checkFields
Removing a Book from the list
Removing a book from the list is simple as well. We first check that a row is selected and then perform the NSMutableArray’s “removeObjectAtIndex_” method passing it the selected row index. We then reload the table data to reflect our deletion and update our saved file as well.
on removeBookFromList_(sender)
if aTableView's selectedRow as integer is not equal to -1 then
theDataSource's removeObjectAtIndex_(aTableView's selectedRow as integer)
aTableView's reloadData()
my writeToFile()
my clearFields()
end if
end removeBookFromList_
Update addData Method
Add the following two lines in the “addData” method.
-- add to first line
tell bookTitleField to selectText_(me)
-- Add as last line
my writeToFile()
I added a handler to quit the application when the window closes
If you do not want this behavior you should create a way to open the window after it has been closed and remove the following code or return false.
on applicationShouldTerminateAfterLastWindowClosed_(sender)
return true
end applicationShouldTerminateAfterLastWindowClosed_
The GUI
Add in the “Update” and “Delete” buttons and connect them to their respective handlers. I also made some minor adjustments to how the fields resize for a better user experience.
Final Code
Download source
property NSMutableArray : class "NSMutableArray"
property NSImage : class "NSImage"
script PartFourAppDelegate
-- Inheritance
-- Our class is a sub-class of NSObject
property parent : class "NSObject"
-- IBOutlets
-- Interface Builder considers an outlet as any
-- property with "missing value" as its initial value
property aTableView : missing value
property aWindow : missing value
property bookTitleField : missing value
property authorField : missing value
property statusField : missing value
-- Bindings
property theAuthor : ""
property bookTitle : ""
property theStatus : ""
-- Other properties
property doubleClickedSelectedRow : -1
property theDataSource : {}
property FILE_PATH : POSIX path of ((path to desktop as string) & "BookList.plist")
-- IBActions (button clicks)
-- Interface Builder considers an action as any
-- single parameter method ending with an underscore
on addData_(sender)
tell bookTitleField to selectText_(me) -- forces all fields to complete editing so that our properties are up to date ( thanks to Shane Stanley )
if my checkFields() then
return
end if
if theStatus is "" then
set theStatus to 0
end if
set newData to {bookTitle:bookTitle, theAuthor:theAuthor, theStatus:theStatus}
theDataSource's addObject_(newData)
aTableView's reloadData()
my clearFields()
aWindow's makeFirstResponder_(bookTitleField)
my writeToFile()
end addData_
on removeBookFromList_(sender)
if aTableView's selectedRow as integer is not equal to -1 then
theDataSource's removeObjectAtIndex_(aTableView's selectedRow as integer)
aTableView's reloadData()
my writeToFile()
my clearFields()
end if
end removeBookFromList_
on updateBookInfo_(sender)
tell bookTitleField to selectText_(me)
if my checkFields() then
return
end if
set updatedBook to {bookTitle:bookTitle, theAuthor:theAuthor, theStatus:theStatus}
theDataSource's replaceObjectAtIndex_withObject_(doubleClickedSelectedRow as integer, updatedBook)
aTableView's reloadData()
my writeToFile()
my clearFields()
end updateBookInfo_
##################################################
# Methods
on writeToFile()
if not theDataSource's writeToFile_atomically_(FILE_PATH, true) then
set messageText to "There was an error writing to file"
display dialog messageText buttons {"Ok"} default button 1
end if
end writeToFile
on clearFields()
-- Clear fields
bookTitleField's setStringValue_("")
authorField's setStringValue_("")
setTheAuthor_("")
setBookTitle_("")
end clearFields
on checkFields()
set missingValues to false
repeat with anItem in {theAuthor, bookTitle}
if anItem is in {"", missing value} then
set missingValues to true
display dialog "The Title and/or Author fields are empty!" with icon 0 buttons {"Ok"} default button 1
return
end if
end repeat
return missingValues
end checkFields
##################################################
# TableView
on tableView_objectValueForTableColumn_row_(aTableView, aColumn, aRow)
if theDataSource's |count|() is equal to 0 then return end
set ident to aColumn's identifier
set theRecord to theDataSource's objectAtIndex_(aRow)
set theValue to theRecord's objectForKey_(ident)
if ident's isEqualToString_("theStatus") then
if theValue's intValue() = 0 then
set theValue to NSImage's imageNamed_("green")
else
set theValue to NSImage's imageNamed_("red")
end if
end if
return theValue
end tableView_objectValueForTableColumn_row_
on numberOfRowsInTableView_(aTableView)
try
if theDataSource's |count|() is equal to missing value then
return 0
else
return theDataSource's |count|()
end if
on error
return 0
end try
end numberOfRowsInTableView_
on tableView_sortDescriptorsDidChange_(aTableView, oldDescriptors)
set sortDesc to aTableView's sortDescriptors()
theDataSource's sortUsingDescriptors_(sortDesc)
aTableView's reloadData()
end tableView_sortDescriptorsDidChange_
on tableDoubleClicked()
if aTableView's selectedRow as integer is not equal to -1 then
my setDoubleClickedSelectedRow_(aTableView's selectedRow as integer)
set thisBook to theDataSource's objectAtIndex_(doubleClickedSelectedRow as integer)
my setTheStatus_(thisBook's valueForKey_("theStatus"))
my setBookTitle_(thisBook's valueForKey_("bookTitle"))
my setTheAuthor_(thisBook's valueForKey_("theAuthor"))
end if
end tableDoubleClicked
##################################################
# Application
on awakeFromNib()
-- Set double-click action on table view
aTableView's setDoubleAction_("tableDoubleClicked")
set theDataSource to NSMutableArray's alloc()'s initWithContentsOfFile_(FILE_PATH)
if theDataSource is equal to missing value then
set theDataSource to NSMutableArray's alloc()'s init()
set theData to {{bookTitle:"A Christmas Carol", theAuthor:"Charles Dickens", theStatus:1}, {bookTitle:"The Adventures of Huckleberry Finn", theAuthor:"Mark Twain", theStatus:1}, {bookTitle:"The Adventures of Tom Sawyer", theAuthor:"Mark Twain", theStatus:0}, {bookTitle:"War and Peace", theAuthor:"Leo Tolstoy", theStatus:0}}
theDataSource's addObjectsFromArray_(theData)
end if
aTableView's reloadData()
end awakeFromNib
on applicationWillFinishLaunching_(aNotification)
-- Insert code here to initialize your application before any files are opened
end applicationWillFinishLaunching_
on applicationShouldTerminate_(sender)
theDataSource's release()
return true
end applicationShouldTerminate_
on applicationShouldTerminateAfterLastWindowClosed_(sender)
return true
end applicationShouldTerminateAfterLastWindowClosed_
end script
Until next time, Happy coding!