Best Strategy for Determining Complex References

I wonder if the experienced coders here could share their strategy for determining the reference to deeply nested objects?

For example, I am trying to correctly refer to (according to the hiearchy shown within the outline view of Interface Builder) a table view, in a scroll view, in a view, in a split view, in a view, in a split view, in a window.

I have tried something like this:

set theTableView to table view “tableData” of scroll view “mainTable” of view “listView” of split view “vertSplit” of view “rightHalf” of split view “horizSplit” of front window

I have also tried:

  1. window 1 instead of front window
  2. getting rid of all the view references (ie. table view … of scroll view … of split view … of split view … of window 1)
  3. using only numbers instead of name references.

My window has two split views, looking much like Mail (view on left, and the right half is split into two) with my table in the upper right.

While I’m curious how to solve this particular case, I’m more interesting in learning what strategies you have in general for accurately identifying an object’s “path” as it were.

I wish there was some easy way I could, for example, “log theObject” and have it show the chain of references, or perhaps “log the path to theObject” or something of that nature.

Am I ignorant of a really simple strategy everyone else is using? There are so many times when I struggle to find the correct reference to what ever object I’m trying to reach out to.

Respectful thanks in advance,

K. M. Lawson
http://foolsworkshop.com/applescript/

Model: MacBook Pro
Browser: Safari 525.18
Operating System: Mac OS X (10.5)

There’s no automated way to reliably get perfect references to objects in your projects. There was an attempt made maybe about two years ago by a small handful of people (myself included) to create a good tool for doing this, but it never happened. There are just too many inconsistencies and weird applescript obstacles to getting it right. Really the only way is just having experience with IB and Xcode enough to be able to just do it manually. Eventually you’ll get it right, even though it may take a while to learn all of the quirks.

My best advice is to never hard-code a reference at all. I find it more practical to capture a reference to the object in a handler such as ‘awake from nib’, and then store that reference in a persistent variable. To start, create a property variable in the head of your script… such as “tableData” if using your variable names. As an aside, I prefer to name my objects carefully and descriptively, and then name my variable the exact same thing so I can keep the two in sync better. For example, if you were building a mail app and were referencing the messages table, you might name it “MessagesTableView”. Using this example, I’d also name my variable “MessagesTableView”. A name such as “tableData” actually might suggest that it might be a data source or a list of the table’s contents, rather than a reference to the table itself, so I’d persanally consider that name too vague and not descriptive enough. When your projects get really complex, a name like ‘tableData’ will just end up being really complicated to look at quickly and know what it refers to.

Once you have your object and property variable set up, then go into IB, and connect the object to it’s ‘awake from nib’ handler. Go back into Xcode, and find the 'awake from nib handler. Set up an if statement that evaluates the call from the table view and simply grab the reference contained in the “theObject” variable of the handler and stuff it in your new property variable. Now, at any point (almost, see following consideration/exception) in your script you can just use “MessagesTableView” in place of any reference to the object that you would have otherwise used. This creates continuity in your code, eliminates ALL hard-coding errors related to reference paths, and makes it really easy to identify objects. *The only consideration you have to make is that the awake from nib handler is called after the ‘will finish launching’ handler and also may not be called if you have your window set to open at launch. If you note these two circumstances and plan around them, this technique makes working with most long references a thing of the past.

You can also use the awake from nib of the main window or launched handler of the app itself to set up all of your variables at launch-time, too. This way, you don’t have to connect tons of objects to their own awake from nib handler. You’ll still have to hard-code some of your references, but doing it this way you’ll only have to do it once, and then use the reference variables from then on. Many times you’ll create your own init handler that you can run at launch that creates all of your references and performs all of your other initialization tasks.

Here’s a simple example of these techniques…

property MainWindow : null
property MessagesTableView : null

on awake from nib theObject
	if name of theObject is "MainWindow" then
		set MainWindow to theObject
		initializeApp()

	else if name of theObject is "MessagesTableView" then
		set MessagesTableView to theObject

	end if
end awake from nib

(* An init routine that you run at launch time *)
to initializeApp()
	set title of button "someButton" of window "anotherWindow" to "Self Destruct"
end initializeApp

Jobu, thanks for your detailed and thoughtful response detailing your strategy for dealing with this problem.

Although I haven’t tested this in my own implementation (will post some code when I do to share the result), your suggestion may help in a number of cases and seems like a sound strategy and will be very helpful. Thank you again.

I will also take your advice of making much more detailed names. I chose the ones I used in the example during a desperate attempt to reach my deeply nested table.

There is one aspect of this where I still wonder what the best approach will be. Since the table (let us use your mail app as an example) is in one document and this document-based app can potentially have several of these documents open, if I set a property to point to MessagesTableView, this will get overwritten whenever a new document is created, right?

Any suggestions for the best strategy to keep track of MessagesTableView when there are potentially one of these objects in all open windows? Some way, for example to reference “MessagesTableView of front window”

Or is it smarter, perhaps, to, instead of using awake from nib, have MessagesTableView continually updated whenever the window becomes the front window? But this seems difficult since, unlike the “become main” handler in the parent window for MessagesTableView there is no “become main” handler for all nested objects.

Model: MacBook Pro
Browser: Safari 525.18
Operating System: Mac OS X (10.5)

That’s a great question. I’ve had in the back of my mind to create some tool to do this for me. But in the mean time, here are a few ad-hoc tools.

You can run your application, click in the object of interest (eg in a text field or select a row in a table), so that the object is selected in some way. Then run this script in Script Editor:

tell application "My Application Name" -- change to be your program
   get first responder of front window
end tell

That will give you the full hierarchy of the selected object. Unfortunately, it refers to each object in the hierarchy by id, not name, but you can see the structure and locate the object reference that you missed in your code.

Another useful method is to navigate to the object in Interface Builder in the browser (columns) view mode. Then you can see the hierarchy of the objects to get there.

Tom
BareFeet

Barefeet, thanks a lot that was also very helpful, and helped me solve the specific example I brought up in my first posting.

I’ll wait a few more days to see if there are others who chime in with their own strategies for approaching this problem, and then I’ll put together the various stuff put up here in a posting over at http://foolsworkshop.com/applescript/ for beginner’s reference.

I still hope that jobu’s idea for an AppleScript Studio reference will take off. Many of us are willing to hunt through a mix of example code, MacScripter forum postings, the AppleScript Studio mailing list archives, Apple documentation, and Neuburg’s book in the hope of finding these kinds of answers, but I still feel there is a lack of a well-written and straightforward problem-oriented approach that combines basic reference with simple examples of approaching common problems that often get mentioned here.

I usually do as jobu writes and connect whatever I want to an ‘awake from nib’ handler. Then inside the handler I write an if statement for the objects name and inside the if statement I log theObject. Then I can see the reference in the Console.

I love jobu’s mention of having a variable set to reference the object, then using that variable in your code. I could have used this recently because I redesigned the interface of my application and thus all my references were changed. If they were all in variables it would have been easy to change the references. As it was find/replace came in handy but it was a chore.

I’m curious about one thing jobu wrote…

I’m not sure what he means by the awake from nib handler not getting called if your window is set to open at launch. My app is set up this way and it calls the awake from nib handler??? Could you explain more?

I just re-read what jobu wrote and finally understood everything. He’s saying to set your variable to theObject and don’t even hard-code in the reference. The variable will get the reference on its own in the awake from nib handler. Wow! That’s a great suggestion. Then my problem of redesigning my interface wouldn’t have caused my reference problems at all! Wow! That’s so simple yet I never would have thought of that.

Thanks again jobu.

But I still have the question I posted above. Under what circumstances would the awake from nib handler not be called? An example please if you have one.

It’s not that it’s not called, it’s just not called when you would expect. There is a quirky behavior that you find when you have a window set to be ‘Visible at Launch’ in ASStudio projects. Under normal circumstances, awake from nib handlers are called for all objects in the application and in the view hierarchy before any other handlers are called (see the awake from nib documentation for the order in which launch-time handlers are called). This is great because you can usually do all of your initializations safely without worrying about whether or not other references you make in other objects’ awake from nib handlers will be valid. BUT…when you have a window that is set to be visible at launch, it is actually called to become visible even before the ‘will finish launching’ handler of the app… LONG before the other objects awaken in the normal course of their own awake from nib handlers. It is possible, although uncommon, to have specific circumstances cause problems with your references, because objects aren’t initializing in a predictable manner. You could end up making a reference to an object that hasn’t awakened from nib yet, and you get an error or it skips the statement and you get a null reference. The best way to work around this problem, is to never set your windows to be visible at launch, and to display them manually in your code using the launched handler of the app instead. This way you can be certain that the objects have been created in the proper sequence.

Let’s look at a the sequence of events given the simple example of a window being shown at launch time, first using the visible at launch and then using the launched handler to do it manually…

1. Visible at launch

  • ‘opened’ (window) ← Not good
  • ‘will finish launching’ (app)
  • ‘awake from nib’ (app)
  • ‘awake from nib’ (window)
  • ‘awake from nib’ (other objects)
  • ‘launched’ (app)

2. Using launched handler

  • ‘will finish launching’ (app)
  • ‘awake from nib’ (app)
  • ‘awake from nib’ (window)
  • ‘awake from nib’ (other objects)
  • ‘launched’ (app)
  • ‘opened’ (window)

As you can see, the window opens even before it’s own awake from nib handler is called. This could really mess you up if you get sequence 1 when you’re assuming sequence 2.

Here’s a practical example…
Create a window (“Window”). Add a button to it (“Button”). Connect the window to its ‘awake from nib’ and ‘opened’ handlers. Connect the app to its ‘launched’ handler. In Xcode, add a property variable for the button reference. Get a reference to the button in the awake from nib handler of the window. In the opened handler, try to set the title of the button using the reference.

property theButton : null

on awake from nib theObject
	if name of theObject is "Window" then
		set theButton to button "Button" of theObject
	end if
end awake from nib

on opened theObject
	set title of theButton to "Schmoodge"
end opened

on launched theObject
	show window "Window" --> Use this instead of the 'Visible at launch' option
end launched

Now, to test the problem we’re discussing, set the window to be visible at launch. When you run the app, you’ll get an error, stating that you can’t set the title of null. Since the reference to the button is created long after the opened handler is called, there’s no object in the variable to set the title of. Now, add the code to display the window to the launched handler, and disable the visible at launch option. You should get the desired result.

This doesn’t just happen in the ‘opened’ handler, it can effect you at unexpected times when you make calls to other handlers or subroutines, and can be difficult to troubleshoot. You may ask why I use the launched handler to open the window, and not some other handler. You could show the window in it’s awake from nib handler too, but you still may run into problems if you have other references to make. The launched handler is “safe”, because if you set up your initialization properly you should be able to be certain that all of your objects’ references are sound before you officially “launch” the app. i.e. show it to the user and let them start using it. Notice that the “other objects” awaken from nib after the window does. The awake from nib process should be hierarchical, so it starts at the highest object and works it way through all of the subviews.

Try moving the ‘show window…’ line in the launched handler to just before the ‘set theButton…’ line in the awake from nib handler. Ka-pow! No go, huh? The button hasn’t awakened from nib on it’s own terms yet. Now put it after that line, and it should work. Making a call to the button first has caused it to be instantiated, so it does exist after you’ve made a hard-coded reference to it. Beware, though, that it too… like the window… may not be officially awakened from nib yet, it may just be instantiated and may not have fired it’s awake from nib handler. These are weird technicalities that can only be discovered through trial and error, and will make you pull your hair out until you learn of them the hard way.

Logically, you would expect that the window must be awakened before it’s ever displayed on the screen. Since it’s not in this circumstance, you just need to be aware of it and plan accordingly. My point was that you have to have a really good grasp of how everything in your app is initialized and configured to avoid these quirks. While sometimes it’s nice to simply click a button and have everything work magically, there are sometimes odd side effects or conflicts you may not anticipate. Being verbose, thorough, and sometimes doing things the hard way can be more intelligent and reliable.

Thanks so much jobu. I ran your examples and now I understand. You’re right when you made the above statement. Most of us, I’m sure, are just learning and hacking as we go along… myself included.

I agree with this. The automatic way may seem easier but you’ll always run into a few oddities when things get complex that will cause you headaches.

Happily I now can do it the hard way with some understanding of what and why I’m doing it. Thanks you for taking the time to thoroughly explain it. I’m in the process of changing my latest app to use references as we’re talking about because I have a couple more interface changes to make. I would have ran into this problem exactly very soon. You just saved me a ton of time troubleshooting that one. I’ll be doing as you suggest and awaking the window myself, the hard way! :smiley:

That’s a great approach, but there seems to be one major problem. It seems that the ASS system doesn’t send an “awake from nib” event for objects in a tab view item, unless it is the current tab view item. So, if I have a tab view containing three tab view items, the awake from nib handler is only invoked for the tab view item that is active/selected/current, which ultimately means however I left it in Interface Builder. None of the objects in the other two tab view items get an awake from nib event until (if ever) the user activates each tab view item.

What’s worse is that ASS seems to incorrectly reference objects in a tab view item when sending through the awake from nib handler. They should be referenced as:

object id <objectID> in view of tab view item id <tabViewItemID> in tab view id <tabViewID>

but instead show the hierarchy:

object id <objectID> in view id <someOtherViewID>

That would be fine, except that the reference it provides only works as long as that tab view item remains current. As soon as the user switches to a different tab view item, the stored object references all appear corrupted, missing all the classes, eg:

«class » id <objectID> of «class » id <someOtherViewID>

and they can’t be used.

For instance, the awake from nib handler attached to a slider in a tab view item (if it’s the current tab view item when awoken) gives the reference:

slider id 584 of view id 583 of tab view id 574 of window id 561 of application "Records"

which I can store in a variable. But as soon as I switch to a different tab view and get that same variable, it’s value appears to have changed:

«class » id 584 of «class » id 583 of application "Records"

and if I try to do anything with that reference (such as set or get a property of that object) it fails.

Is this a known bug?

Is there a way around it?

Thanks,
Tom
BareFeet

I noticed this too. I have a 3-tabview window for preferences in my application. Only the tab that is displayed when the pref window first opens goes through the ‘awake from nib’ handler at program launch. The other 2 don’t awake until the user activates them. I just do all of my initialization stuff in the ‘awake from nib’ handler right after I define my tabview object. As such they do get initialized before they are displayed.

This doesn’t happen to me at all. I’m not sure why you think it doesn’t work but the prefs in my app work fine, so I can’t say I see this behavior. I can open/close the prefs window, switch between tabs etc., come back to it later, and all the objects are referenced correctly in all 3 tabs.

I never logged the tabview objects and looked at the references, but they all work fine. Of course buttons in the tab views don’t matter because you just get them by name in the ‘on clicked’ handler but I do have some text fields in them that I read and write data to, and they work fine.

I should have pointed out that the references work whenever the containing tab view item is current, but not when another tab view item is current.

So, I get this situation:

  1. awake from nib on objects in current tab view → references work, can store in a variable.

  2. Access objects via variable works.

  3. Switch current tab view item. Variables to objects in original tab view item no longer work.

  4. Switch back to original tab view item. Variables referring to objects in original tab view item now suddenly work again.

So that explains why it works for you when you “come back later”. But try using your variables when the enclosing tab view item of those objects isn’t current and it will fail.

All these problems seem to stem from a bug with ASS not correctly referencing objects supplied to the awake from nib handler. As I mentioned before, ASS seems to refer to objects as within some abstract view (probably attached to the “current view”) within a window, without the actual hierarchy. So as soon as the current view changes, that temporary view is no longer valid and any stored references to objects within the previous current view no longer work. They work again when the current view switches back.

Tom
BareFeet
http://www.tandb.com.au/applescript/