Tuesday, September 23, 2014
  • Index
  •  » unScripted
  •  » AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

#1 2009-09-04 02:19:46 pm

Craig Williams
Administrator
From: Ft. Smith, AR
Registered: 2006-12-06
Posts: 887

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.

 - Click to enlarge
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
 - Click to enlarge
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

 

#2 2009-09-06 10:09:46 pm

robdut
Member
Registered: 2009-09-02
Posts: 277

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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

 

#3 2009-09-06 10:48:14 pm

Craig Williams
Administrator
From: Ft. Smith, AR
Registered: 2006-12-06
Posts: 887

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

Hi Rob,

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

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

 

#4 2009-09-07 05:30:15 am

robdut
Member
Registered: 2009-09-02
Posts: 277

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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

 

#5 2009-10-26 01:57:19 pm

Heiner
Member
From: Germany
Registered: 2007-08-25
Posts: 137

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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

 

#6 2009-11-02 10:17:14 pm

Craig Williams
Administrator
From: Ft. Smith, AR
Registered: 2006-12-06
Posts: 887

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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

Offline

 

#7 2009-11-05 01:44:24 am

mbs402
Member
From: Plano, Texas
Registered: 2009-04-07
Posts: 53
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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

 

#8 2011-06-06 06:15:15 am

crgand
Member
From: Milano, Italia
Registered: 2008-11-15
Posts: 39
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

Is there a way to modify table raws order by dragging it?

Offline

 

#9 2011-06-06 07:41:57 am

StefanK
Member
From: St. Gallen, Switzerland
Registered: 2006-10-21
Posts: 10519
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

crgand wrote:

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

 

#10 2011-06-16 07:06:10 pm

crgand
Member
From: Milano, Italia
Registered: 2008-11-15
Posts: 39
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

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

 

#11 2011-06-24 08:24:50 am

crgand
Member
From: Milano, Italia
Registered: 2008-11-15
Posts: 39
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

Craig Williams wrote:

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

 

#12 2011-06-24 08:34:52 am

StefanK
Member
From: St. Gallen, Switzerland
Registered: 2006-10-21
Posts: 10519
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

crgand wrote:

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 08:38:00 am)


regards

Stefan

Offline

 

#13 2011-06-24 10:03:01 am

crgand
Member
From: Milano, Italia
Registered: 2008-11-15
Posts: 39
Website

Re: AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

StefanK wrote:

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

 
  • Index
  •  » unScripted
  •  » AppleScriptObjC in Xcode Part 4 - Saving, Updating & Deleting

Board footer

Powered by FluxBB

[ Generated in 0.062 seconds, 10 queries executed ]

RSS (new topics) RSS (active topics)