AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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.


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!

Download source

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


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

Hi Rob,

Ok, looks like I needed a little more error checking. :slight_smile:
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.


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

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

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.


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

You will need to add delegate methods to handle editing.
Look in the documentation under NSTableViewDelegate Protocol Reference.

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

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


	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.

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:


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.

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


property FILE_PATH : missing value

on awakeFromNib()
	set FILE_PATH to POSIX path of (path to preferences folder) & "BookList.plist"
end awakeFromNib


Solved, thanks, thanks, thanks a lot.