You are not logged in.
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
Download source
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.
Applescript:
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.
Applescript:
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.
-- This syntax does not work
set bookTitle to thisBook's valueForKey_("theStatus")
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
// Getter method
-(NSString *)bookTitle
{
return bookTitle;
}
// Setter method
-(void)setBookTitle:(NSString *)title
{
[title retain];
[bookTitle release];
bookTitle = title;
}
tableDoubleClicked Handler
Applescript:
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.
Applescript:
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
Applescript:
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
Applescript:
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.
Applescript:
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.
Applescript:
-- 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.
Applescript:
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
Applescript:
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!
Download source
Offline
Hi Craig,
I noticed a couple things:
If the user presses update without double clicking a row first to fill the text fields, the top row gets updated with blanks. This is true even if another row is selected (without double clicking.)
So I added a check
Applescript:
if on updateBookInfo_(sender)
tell bookTitleField to selectText_(me)
if bookTitle = "" or theAuthor = "" then
display dialog "The Title and/or Author fields are empty!" with icon 0
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_
This stops that happening unless you double click an item and update it. Then any accidental "update" still blanks the top row again and my check dialog doesn't appear. This seems odd since supposedly the fields have been cleared. This means bookTitle or theAuthor have not been cleared even though the text fields are blank.
I don't care about this working - I am just trying to understand the relation of the properties to the bindings, text fields etc..
Rob
Offline
Hi Rob,
Ok, looks like I needed a little more error checking.
Thanks for catching it.
Maybe six tutorials this week was toooo much.
With bindings and properties we have to clear both the text fields and the property variables.
Applescript:
bookTitleField's setStringValue_("")
authorField's setStringValue_("")
setTheAuthor_("")
setBookTitle_("")
To access a property we use its "get" method which is just its name. To set its value we use its
"set" method as show above.
I have updated the tutorial to reflect these error checks.
Check it out and let me know if what I did makes sense.
Regards,
Craig
Offline
Thanks Craig,
That makes perfect sense. I now understand the relationship of the bindings and properties. I learn by tinkering with sample code.
Tutorial 5 is also excellent. These simple instructions are of great help to us as they really bridge the gap.
Keep them coming!
Rob
Offline
Hi Craig,
I tried to use your example as a template for my task:
In a table view the user should add a list of names from the clipboard into the first column, the next columns should be editable for uses insertions of numbers, and the last column should sum up the contents of the columns beforehand.
My status is that the insertion of names works, but the columns are not editable. What ever I insert, it will deleted. In IB I set the columns to be editable.
I don't know how to come to an end. My be you have a few hints for me.
Thanks in advance
Heiner
Here is a sketch of my script.
Applescript:
property parent : class "NSObject"
property theDataSource : {}
-- IB Outlets
property mainWindow : missing value
property aTableView : missing value
-- other
property newName : missing value
property newSum : missing value
--IB Action (menu item)
on setNames_(sender)
try
set theContents to the clipboard as string
end try
set oldTextItemDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to return
set theCount to (count text items of theContents)
if last text item of theContents is "" then
set theCount to theCount - 1
end if
repeat with i from 1 to theCount
set newName to (text item i of theContents)
set newData to {theName:newName}
-- set newData to {theName:newName, nr1:missing value,nr2:missing value,nr3:missing value, theSum:missing value}
theDataSource's addObject_(newData)
aTableView's reloadData()
end repeat
set AppleScript's text item delimiters to oldTextItemDelimiters
end setNames_
on tableView_objectValueForTableColumn_row_(aTableView, aColumn, aRow)
if theDataSource's |count|() is equal to 0 then
return
end if
set ident to aColumn's identifier
set theRecord to theDataSource's objectAtIndex_(aRow)
set theValue to theRecord's objectForKey_(ident)
-- ???????? (to be solved)
return theValue
end tableView_objectValueForTableColumn_row_
on numberOfRowsInTableView_(aTableView)
-- as in 'PartFour'
end numberOfRowsInTableView_
on tableView_sortDescriptorsDidChange_(aTableView, oldDescriptors)
-- as in 'PartFour'
end tableView_sortDescriptorsDidChange_
on awakeFromNib()
if theDataSource is equal to {} then
set theDataSource to NSMutableArray's alloc()'s init()
end if
end awakeFromNib
Offline
You will need to add delegate methods to handle editing.
Look in the documentation under NSTableViewDelegate Protocol Reference.
Offline
Craig,
You refer to setTheStatus_ , and I tried to find :
On setTheStatus_(thisBook's valueForKey_("theStatus"))
But could not find it in any of the five documents.
I have been trying to preselect a radio button, and cannot figure out a way to do it.
I created a binding on the elementIndex to a variable. If I change the selected radio button manually I can get the selectedIndex and display it, but I can't for example select the 3rd radio button while it loads the page.
I also tried to get the value of the selected radio button instead of the index, but could not make that work
I would like to save the selected radio button to a plist, then reload it the next time I launch.
I would like to trigger a script when a different radio button is selected. I am not sure if I will have to select each radio button individually and control drag to the app delegate, or whether you can select all three and do the control drag.
Thanks for any help you can provide...
Bill Hernandez
Plano, Texas
Offline
Is there a way to modify table raws order by dragging it?
Yes, but it's not trivial, because you have to rearrange (delete and insert) the table view items programmatically,
even using NSArrayController
regards
Stefan
Offline
Applescript:
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
I wish to modify checkFields() so that dialog message is show as sheet.
I try to use Shane's showAlertAsSheet_(sender) from ASOC Explored book adding NSAlert+MyriadHelpers but no sheet appear and no clue from console.
Offline
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.Applescript:
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
I've build (Xcode 4 OSX 10.6.7) PartFour.app and work fine on my computer but when I copy the app to another machine I get "There was an error writing to file" error when saving book list data.
Same error if I use ~/Library/Preferences folder:
Applescript:
property FILE_PATH : POSIX path of ((path to preferences folder from user domain as string) & "BookList.plist")
Is a file permissions error?
I've used this code in my app and I can't use it on other computer.
Thanks in advance for your help.
Offline
Same error if I use ~/Library/Preferences folder:
Applescript:
property FILE_PATH : POSIX path of ((path to preferences folder from user domain as string) & "BookList.plist")
I've used this code in my app and I can't use it on other computer.
NEVER EVER use a relative path specifier (path to …) in a property.
It will be set at compile time and remains persistent.
Better set the property somewhere else in one of the initial handlers
Applescript:
property FILE_PATH : missing value
on awakeFromNib()
set FILE_PATH to POSIX path of (path to preferences folder) & "BookList.plist"
end awakeFromNib
Last edited by StefanK (2011-06-24 07:38:00 am)
regards
Stefan
Offline
NEVER EVER use a relative path specifier (path to …) in a property.
It will be set at compile time and remains persistent.
Better set the property somewhere else in one of the initial handlers
Solved, thanks, thanks, thanks a lot.
Offline