Defining a custom Selector within an ASObjC Script Library (Mavericks)

I am converting my old-style Applescript script libraries (used with the load script command) to the new Mavericks’ Script Libraries. Learning from the most useful video tutorial on macosxautomation.com I have, of course, seen huge performance gains when using ASObjC in these libraries. It’s heartening to see Apple breathe new life into Applescript this way (if you’re reading this, Tim…).

I have progressed from basic string manipulation and am now trying to harness the power of ASObjC in sorting lists (and later, I hope, lists of records). I know it’s almost a trivial matter to sort a one-dimensional list with something like this (no error-checking):

on sort list inList
	set anNSArray to current application's NSArray's arrayWithArray:inList
	set sortedArray to anNSArray's sortedArrayUsingSelector:"compare:"
	return sortedArray as list
end sort list

But I now want to sort a list of lists, based on the value of item x in each sublist. How can I stipulate my own selector instead of the built-in “compare:” above? Perhaps theMutableArray below should be subclassed? Is that even possible with Applescript libraries?

What I have so far:

on sort list of lists inList by sortElement
	
	-- attempt to use ASobjC to sort a list of records...  WIP, very WIP...
	set theMutableArray to current application's NSMutableArray's alloc's init()
	set eSort to sortElement
	set listCount to count of inList
	repeat with i from 1 to listCount
		-- convert dates to NSDates, etc?  In progress...
		(theMutableArray's addObject:(item i of inList))
	end repeat
	
	-- Array seems to be set up as expected:
	log (theMutableArray's objectAtIndex:sortElement)'s item 1 as text
	
	-- this fails
	set theSortedArray to theMutableArray's sortedArrayUsingSelector:"compareItem:"
	
	return theSortedArray as list
	
end sort list of lists

on compareItem:anItem
	return
end compareItem:


I know I could just carry on using the excellent ASObjC Runner, but I’m trying to learn to roll my own…

Any help/pointers gratefully received!

Quentin

What are you going to use a sort array with sub arrays for?

I don’t see any silver bullet for this one. I see you sort every sub array, then write a comparatator that runs over two arrays at at a time, either until they are exhausted, and proved to be alike, or that they were indeed different, and the swap can take place. Beyond what you get for free when it comes to sorting, I am not sure that it can be done in ASoC since you will have to write your own (robust) comparator.

It would be fun to be proved wrong on this one. :slight_smile:

Edit

I finally grasped your question: sort an array of arrays, based on a value in a fixed position in that sub arrray.

Stefan really knows what he is talking about, and he doesn’t resort you to Objective-C. You’ll have to write the comparator, so that it takes arrays as its parameters, and maybe an index to the element you want to compare on.

But this is highly doable, and should prove to be an interesting excercise.

Hi,

sorting in Objective-C is based on comparison selectors which must return a comparison result (-1. 0 or 1)
For example a number comparison method looks like


on compare:theNumber
	if myself < theNumber then
		return -1
	else if myself = theNumber then
		return 0
	else
		return 1
	end if
end compare

myself is the receiver, the instance of the parent class

McUsrII,

This is what I mean by sorting a list of lists (two-dimensional list) by an element within the sublists:

set l1 to {"apples", "pears", "melons"}
set l2 to {"pigs", "lions", "horses"}
set l3 to {"alpha", "beta", "gamma"}
set ll to {l1, l2, l3}

-- sorting ll by the second element will compare "pears", "lions" and "beta" and order l1, l2 and l3 within ll according to the ranking of their second elements.
set sortedBy1 to sort list of lists ll by 1
-- sortedBy1 contains {{"alpha", "beta", "gamma"}, {"apples", "pears", "melons"}, {"pigs", "lions", "horses"}}

set sortedBy2 to sort list of lists ll by 2
-- sortedBy2 contains {{"alpha", "beta", "gamma"}, {"pigs", "lions", "horses"}, {"apples", "pears", "melons"}}

It is exactly what Shane’s rearrange list of lists in his ASObjC Runner does.

Stefan,

I think if I were doing this in pure Obj-C and Cocoa I wouldn’t have a problem. I could call a custom selector from my own class. But I should not have left my compare: method blank in my previous example: it gave the impression I didn’t know how it worked. I had tried your approach but realised the custom compare: method was not being called at all (a log statement failed to print). Actually, for my two-dimensional array wouldn’t it look something like this:

global eSort

on sort list inList
	set anNSArray to current application's NSArray's arrayWithArray:inList
	set sortedArray to anNSArray's sortedArrayUsingSelector:"compare:"
	return sortedArray as list
end sort list


on sort list of lists inList by sortElement
	
	-- attempt to use ASobjC to sort a list of lists...  WIP
	set theMutableArray to current application's NSMutableArray's alloc's init()
	
	-- save the sort index for use in comparison selector
	set eSort to sortElement
	
	set listCount to count of inList
	repeat with i from 1 to listCount
		-- this doesn't seem necessary: the inside list is converted automatically to an NSArray
		--set subArray to (current application's NSArray's arrayWithArray:(item i of inList))
		--(theMutableArray's addObject:subArray)
		-- convert dates to NSDates, etc?  In progress...
		(theMutableArray's addObject:(item i of inList))
	end repeat
	
	-- Testing access: array seems to be set up as expected (no error-checking for bounds here clearly):
	log (theMutableArray's objectAtIndex:sortElement)'s item 3 as text
	
	-- this fails
	set theSortedArray to theMutableArray's sortedArrayUsingSelector:"compareItem:"
	
	return theSortedArray as list
	
end sort list of lists



on compareItem:anItem
	if item eSort of me < item eSort of anItem then
		return -1
	else if item eSort of me = item eSort of anItem then
		return 0
	else
		return 1
	end if
end compareItem:


Sadly, I don’t think this is going to work within the constrictions of a single ASobjC library file. A headless Cocoa app (like Shane’s ASObjC Runner) increasingly looks to me like the only solution where Cocoa methods must be overridden. I’ve written other scriptable Cocoa apps, so at least I think it’s within my grasp, though now I’d really be reinventing the wheel!

Thanks for your input,

Quentin

sorry, I just wanted to describe the structure of a comparison selector.

For sorting multidimensional array you need a custom comparator, but I doubt that it’s possible in AppleScriptObjC because the appropriate methods require blocks which are not available in AppleScriptObjC

No, there’s a much better approach: an Objective-C framework. You write/find an Objective-C framework, then add it to your library (inside the bundle in a directory called Frameworks), or one of your Frameworks paths, and then add a ‘use framework “”’ statement.

(ASObjC Explorer for Mavericks has a command for adding a Frameworks folder and including frameworks in it in exported libraries; it was designed with this in mind.)

I’m planning, if I ever get any spare time, to put out frameworks with a lot of the stuff from Runner (I have a couple of proofs of concept and they work fine).

This approach is better than an FBA like Runner (or scripting additions) because it also offers a way to store private state.

PS: You can sort on the first or last element using a sort descriptor.

PPS: Sometimes the best solution might be to go back to the code producing the lists of lists and have them produce lists of records instead – that solves the problem more elegantly.

Hi Shane,

I think the use of frameworks is precisely the sort of thing I was hoping for.

I’ve been playing around a while with a test framework (my first). A few issues:

Although you say I can place my framework inside the Frameworks folder within the script bundle (I created the folder), it is not seen by the use framework command: compile error. However, if the framework is placed somewhere else in my frameworks path (like ~/Library/Frameworks) it can be used OK. What am I missing?

In my framework I have made the requisite headers public and set the installation directory to @executable_path/…/Frameworks, if that matters…

I tested the framework with a bare-bones class called TestClass as NSObject. The header is public. There is one class method in that class that simply returns a string. It works: I can assign the return value from this method of TestClass to a variable in my library script and all is well.

However, the minute I try to pass parameter(s) to a method of TestClass, the command fails with:

error “+[TestClass returnString:]: unrecognized selector sent to class 0x10662c198” number -10000

returnString: is defined thus:

  • (NSString *) returnString:(NSString *) inString {
    return inString;
    }

I have not (clearly) done much ASObjC, though I read through your book (V1.0.2) a while back and am revisiting it. Perhaps I need a later edition?

Ideally, I would have liked to have explored your ASObjC Explorer by now; I’m sure it would help a lot. Sadly when I try to launch it I get a dialog box saying ‘“ASObjC Explorer” can’t be opened. You should move it to the Trash’! I’ve tried downloading on two separate machines in Firefox and Safari. OS X 10.9

Meanwhile thanks for your help, Shane. It’s invaluable.

Quentin

Where are you creating the Frameworks folder?

Hang on a couple of days and I’ll have a new one out aimed specifically at ASObjC-based libs in Mavericks. It doesn’t specifically cover adding frameworks, but it does cover lots of other stuff.

Sigh. My apologies. It’s a code-signing issue – I’ve been telling Xcode to sign it with my Developer ID, but it seems it has changed back to using another ID for the latest update. Please try again.

Cheers,

Shane,

Many thanks for the prompt action on ASObjC Explorer.

There have been lots of bumps along the way but I’ve got the two basic sorting commands outlined above operational using a Cocoa framework. Much delight. I think many of the problems I did encounter were due to strange caching behaviour and overwritten frameworks in the Trash refusing to let go. Very odd and frustrating.

I finally figured out where I was going wrong with the placement of the framework. I had been adding a folder called Frameworks to the list of items in the drawer of the Applescript Editor window. This is actually the Resources folder, of course (it does say so!). For all you other newbies, you need to get Finder to open the script bundle and navigate to Contents/Frameworks. Silly mistake.

Stefan, I didn’t understand the full impact of your remark that “you can’t use blocks in ASObjc” until I failed to get these to work even within the framework (the same code using a comparator worked fine within a full Cocoa app). I had thought you were referring to the actual ASObjC script code, where I realised they would probably never exist (imagine Applescript with blocks!). I have had to abandon the comparator method for sortedArrayUsingFunction:context: Seems there are quite a few caveats in writing ASObjC frameworks.

Again, thanks for your help, Shane. Where would we be without you?

Quentin

the argument of sortedArrayUsingFunction is a block function, this is the part starting with the caret (^)
AppleScriptObjC does not support those blocks

Thanks for letting me know!

Remember that once an app has loaded a framework, you have you quit it before it can load a modified version.

FWIW, ASObjC Explorer checks its Resources list, and if it sees a folder called Frameworks it puts it in the correct place, not in Resources. (I decided that was easier than adding another list to the UI.)

ASObjC Runner uses a block comparator method. It shouldn’t make any difference.

You should be able to use a function fine. ASObjC is just (another) wrapper. Heck, Objective-C is just a wrapper around functions…

Shane,

I think I just worked this out! Thanks for confirming.

And I just found why my block comparator method wasn’t picked up first time round: missing #import statement in the top level file (doh!). Works fine now: my solution is much more elegant with just one simple category for NSArray. Have tossed the clunky function. So forget what I said about “quite a few caveats.” Are there any, in fact, in the context of framework writing? Perhaps we must buy your next book to find out, Shane…

Stefan, actually, I think the sortedArrayUsingFunction method does not use a block. Blocks only came in with Snow Leopard, no? sortedArrayUsingFunction is invoked as in this snippet (there is no ^):

.
return [inArray sortedArrayUsingFunction:compareByItem context:NULL];

I just wrote a list-of-records sort routine in one edit (pure ASObjC) with no blunders this time (less error-checking). This thing rocks. Yes, records are much easier to manipulate than embedded arrays.

Thanks again, all, for input.

If you use enums, you need to also build a .bridgesupport file. And you want to steer clear of things like pointers-to-pointers, unless you want to hand write .bridgesupport exception files. But otherwise it’s pretty simple.

This is the future for extending AS. It’s a lot cleaner than scripting additions, not to mention simpler. Just wrap a framework in ASObjC, then give it terminology. The only loss is the ability to use optional parameters, as far as I can see.

The new book is more a beginner’s guide to ASObjC, written around ASObjC-based libraries – it doesn’t get into bundling frameworks. But you guys need to buy it anyway, to keep me off the streets :wink:

I’m glad to see someone else excited by it!