Category Archives: Objective-C
Is it me, or is AppleScript experiencing something of an Indian Summer? It seems everywhere I go, people are talking more about macOS automation, AppleScript and even Apple’s curious hybrid syntax AppleScriptObjC (ASObjC).
Of course, some people have suffered miserably at the hands of AppleScript in the past, and even though the thought of scripting with access to Cocoa APIs through Objective-C is tempting, they fear the AppleScript side of it.
If that’s you, bear in mind that AppleScriptObjC isn’t really “AppleScript + Objective-C” at all. It is actually just a dialect of Objective-C that will be accepted in the (Apple)Script Editor and can be run by an instance of the AppleScript component. In plainer English, you can use Objective-C in an AppleScript without any AppleScript whatsoever!
The point of doing so would be that one could package Objective-C code in a .scpt file (or scptd bundle or AppleScript .app), and also mix whatever scripting language you prefer with calls to Cocoa’s APIs.*
The problem that using ASObjC presents anyone familiar with Objective-C is how to translate ‘pure’ Objective-C into the dialect that Script Editor (and other applescript runners like FastScripts, Keyboard Maestro, Automator, etc) can understand. If you use LateNight Software’s Script Debugger for scripting, you’ll already know that the work is done for you by the app’s built-in code completion. If you’re battling on in Apple’s default Script Editor, you’ll need to do the translation manually.
By way of example, then, here’s some original Objective-C, and below it, a translation that would work in Script Editor:
NSString *aString = @"hello";
NSString *bString = @" world";
aString = [aString stringByAppendingString:bString];
NSUserNotification *notif = [[NSUserNotification alloc] init];
notif.informativeText = aString;
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notif];
set aString to NSString's stringWithString:"hello"
set bString to NSString's stringWithString:" world"
set aString to aString's stringByAppendingString:bString
set notif to NSUserNotification's alloc's init
set notif's informativeText to aString
NSUserNotificationCenter's defaultUserNotificationCenter()'s deliverNotification:notif
As you can see, there’s a direct 1-to-1 correspondence, with the 6 statements in Objective-C paralleled by the 6 statements in AppleScriptObjC.
The main peculiarity is the use of possessive word forms and that variable attribution is done by using
"set X to Y" rather than
"X = Y". Type declaration is done via the idiom
'set <var> to <NSObject>'s <class init method>', which returns an instance of the object just as it would normally. You call instance methods by putting the instance in front of the method just as you would in regular Objective-C (e.g, see line 3 of the examples).
As you can see in the screenshot below showing Xcode and Script Editor, they work in the same way. You’ll notice in Script Editor there is a
'use' statement (equivalent to Objective-C’s ‘import’), and there’s also a whole load of property statements. These latter are peculiar to the ASObjC translation, and don’t have a counterpart in pure Objective-C. All you need to know about these is for each kind of Objective-C object you want to use (NSString, NSArray, whatever*), you’ll want a property statement for it at the beginning of the script. The statement always has the same form:
property <NSObject> : a reference to current application's < NSObject>
I think the best way to think of ASObjC was recently summed up by Sal Saghoian, when he said that ASObjC is “…the ultimate duct tape. You can do anything you want with ASObjC. You own the computer.”
*not all Cocoa frameworks nor all Objective-C objects can be bridged to, but pretty much all the most useful ones are available.
Showing a popover underneath a piece of selected text is a trick widely made use of across OSX, not least in Xcode itself, but also in many other apps. In this post we’re going to look at a simple way to achieve this effect.
1. First of all, in IB drag out a Popover and View Controller and drop them in the Document Outline area. Drag a custom view to the canvas, and hook them all up thus:
delegate –> App Delegate (or class that will control the Popover)
Popover View Controller’s Outlets
view –> Custom View
Click the Popover object in the Outline area, and in the Attributes Inspector, set the Behaviour to Transient.
In the appDelegate.h file, or the .h file of the class that will control your popover, include the PopoverDelegate in the @interface declaration:
@interface AppDelegate : NSObject <NSApplicationDelegate, NSPopoverDelegate>
2. Still in the header file, you need to make sure the window that the NSTextView is in has a View outlet in its header file.
@property (weak) IBOutlet NSView *aView;
For my purposes, I just have a single window in the appDelegate class, so I just created a view property by control-dragging out of the Window’s view object to the header file. You’ll need to switch to the dual-view Assistant editor to do this:
While you’ve got the Assistant editor open, drag out an outlet from the Popover to the .h file and name it ‘popover’. Finally, in the same way create a similar outlet for your TextView.
4. Next, go into the implementation .m file for the appDelegate (or you class). You’ll need an IBAction to trigger the showing of the popover. In my case, I have an ‘Enter’ button the user hits after making a selection attached to a method I called enterSelection:(NSButton *)sender.
In this method, I first get the rect for the user’s selection with:
NSRect rect = [_textView firstRectForCharacterRange:[_textView selectedRange] actualRange:NULL];
That will return a rect in screen coordinates for the selected text. However, I need to convert that into the window’s coordinates with:
NSRect converted = [_window convertRectFromScreen:rect];
Now we’re ready to call the popover by supplying this rect to the first parameter and the View property we created earlier to the second parameter:
[self.popover showRelativeToRect:converted ofView: _aView preferredEdge:NSMinYEdge];
And that’s it. Your popover should show underneath the selected text whenever your method gets called. If you want to see how this is done step by step in Xcode, check out the video:
Here’s a little problem and solution I ran into the other day while using UITableView. I wanted to have a master-detail set up in which the UITableView was segued to after an initial home screen.
The problem occurred whenever I added or deleted something in the UITableView, segued back to the home page and then returned to the table view. Sometimes the table would not update. Going out and back into the view a second time, however, would finally reload my data and show me the changes. Why was my app needing to load the view twice before the table would show any changes?
Logging showed me that something even weirder was going on: Every time I segued into the table view, the viewDidLoad method was being called not once, but twice. So basically, to get my updated data to show, I was actually calling viewDidLoad four times!
I worked through a whole bunch of stackexchange posts related to various problems and solutions with the reloadData method — adding delays, removing it from any edit- or insert- methods and so on, calling it on the main thread — but the problem stubbornly remained.
Then I noticed something else. In my awakeFromNib method, the call to super had somehow got pushed to the end of the method, after a bunch of other set up calls. I’m not sure how it got there, but I did remember seeing a WWDC 2014 Swift video pointing out that one difference between Objective-C and Swift was that calls to super need to be made after doing any initial set up in Swift’s case, but before for Objective-C. Since this was an Obj-C project, what was my call to super doing at the end of the method?
And as if by magic, moving [super awakeFromNib] to the beginning of my awakeFromNib method resulted in viewDidLoad only being called once, and my table view updating correctly.
Though I found quite a few threads with people having a similar problem with UITableView’s needing two calls before updating, I haven’t come across this particular solution to (or cause of) the problem. Hopefully, this post will save someone else a few hours of head scratching!