/dev/trouble
Eric Roller's Development Blog

Use of dealloc

- Posted in macOS by

After spending a few days trying to figure out why my log file was not truncated correctly, I re-discovered a feature of the Objective-C runtime: The dealloc method is never called - at least not to my knowledge.

Therefore, do not add mission-critical code in the dealloc method of a class; use a dedicated custom method instead, for instance:

- (void) close
{
    [theLog truncateFileAtOffset:[theLog offsetInFile]];
    [theLog closeFile];
}

Splitting Disk Images

- Posted in General by

This has been bugging me for a while: How do you split large .dmg files such that you can save them onto multiple CDs (as opposed to an expensive DVD). It turns out, there is no special tool required (although it would be nice if Disk Utility would do the job). No, the functionality is already built-in in the Mac OS. All you need to do is to use the hdiutil in the Terminal to create your partitions:

% cd Downloads
% mkdir parts
% hdiutil segment -o ./parts/large_image.dmg \
    -segmentSize 100m large_image.dmg

Of the resulting files in the parts directory, one can simply double-click the first one in the Finder to mount the entire image:

large_image.dmg
large_image.002.dmgpart
[...]
large_image.010.dmgpart

A not very well documented error message which occurs right at the beginning of the build, typically during compilation of the prefix file. The error is found in a derived file, for instance:

.../EnterMordor.build/DerivedSources/EnterMordor_vers.c:1: error: parse error before ';' token:

[...] const double EnterMordorVersionNumber __attribute__
((used)) = (double);

Observe that there is no valid argument after the equal sign. To find the cause, we must know that derived files are based on our build settings. In my case, the fault lies in the fact that the built setting VERSIONING_SYSTEM is set to apple-generic, but I was missing is a setting for the CURRENT_PROJECT_VERSION variable.

See also Xcode: Help -> Show Build Setting Notes -> Versioning.

According to Chris Hanson's article, the apple-generic versioning system uses agvtool to auto-update all Info.plist files. Also, there will be a TargetVersionString and a TargetVersionNumber variable available to your code. Right now, I you do not like or need all that, so I simply set the versioning system to "none" and continue to manually update the CFBundleShortVersionString and CFBundleVersion settings.

Using a popup menu to allow the user to select the (integer) value of an object can be easily done through binding the selectionIndex. However, what if you wanted to launch an update method when the selection changes, you have two options:

  • Assign a target to each menu item
  • Assign a target to the menu itself

I made the mistake of trying the first option, assigning the same target to each of the menu items. This effectively disabled the selectionIndex binding, rendering the popup menu completely useless.

It appears as if the only solution is to assign a target to the menu, not its items!

If you ask me, this is a bug: binding "enabled" of an NSMenuItem to anything does not make any difference. It seems that the target of the menu item needs to tackle the issue in the conventional manner, i.e. through the method:

- (BOOL) validateMenuItem:(NSMenuItem*)item
{
    if (itemShouldBeEnabled)
        return YES;

    return NO;
}

The downside of this is that the target may need access to other objects that would have beeen simple to achieve through the binding mechanism, sigh.

In a custom NSArrayController, an action was only to be done when we had a selection. The initial attempt was along the lines of:

if ([self selection])
{
    // do something
}

That, however, does not work since even an empty selection returns a proxy object:

(gdb) print-object [self selection]
<_NSControllerObjectProxy: 0x5bfa60>
(gdb) print-object [[self selection] valueForKeyPath:@"db.name"]

So I checked the NSObjectController documentation and was reminded that a NSNoSelectionMarker can be returned by the function, making me think it that the if statement could be changed like this (NOT):

if ([self selection] != NSNoSelectionMarker)
{
    // DOES NOT WORK
}

However, this does not work (but checking the return value of valueForKeyPath: might).

The most elegant solution for an NSArrayController would be to ignore the selection and just to check the (first) selectionIndex like this:

if ([self selectionIndex] != NSNotFound)
{
    // do something
}

There does not appear to be a way to easily define a double-click action for an NSTableView in Interface Builder. The best solution that I have found is via a custom table view class and a dummy action. This requires that the target is available in the nib file, e.g. the window controller or a custom array controller of the table view.

Step one: control-drag a target/action-connection from the table view to the target where the double-click action is defined, but select a different action method, at best a bogus method that does nothing (I like to use "noAction:" for such purposes).

awakeFromNib method to assign a double-click action.

+ (void) awakeFromNib
{
    NSAssert1([[self target] isKindOfClass:[Duck class]],
            @"The target of MyTableView should be Duck, not %@.",
            [[self target] class]);

    // Let our drag-and-drop array controller, i.e. the Duck,
    // handle double-clicks to the cells in the table view.
    [self setDoubleAction:@selector(doubleClick:)];

    // We no longer need the dummy action (noAction:)
    [self setAction:NULL];
}

Two issues today: A simple mini-dialog box with a text field and an ok button. Editing the text field and pressing "ok" did not change the value in the target that is bound to the text field. It turns out, the editing is not finished when we press the button (unless the user presses "tab" or possibly "return"). We can force editing to end through this construct in the action of the ok button:

- (IBAction) ok:(id)sender
{
    // ask the window to take the focus away from the edit field,
    // thus stop editing and take over the value.
    if ([[sender window] makeFirstResponder:sender])
    {
        [NSApp endSheet:[sender window]
             returnCode:NSAlertDefaultReturn];
        [[sender window] orderOut:self];
    }
}

When the value was updated, I got this error in the console:

2006-03-16 23:41:50.940 MyApp[547] ***
-[NSCFString unsignedLongValue]: selector not recognized

This is because the object in the dictionary has been set to a string, not a number:

(gdb) print-object [[data objectForKey:@"teve"] class]
NSCFNumber
...
(gdb) print-object [[data objectForKey:@"teve"] class]
NSCFString

... and the string does not support "unsignedLongValue". This could easily be solved by attaching a NSNumberFormatter to the NSTextField, by which means it is also ensured that one only gets legal numbers (e.g. no floating point values and minimum set to 0).

To enable the debug menu, type in the Terminal application:

> defaults write com.apple.safari IncludeDebugMenu 1

To use Yahoo instead of Google in the search bar, you can hack the Safari binary, for instance with the vi text editor:

> vi /Applications/Safari.app/Contents/MacOS/Safari
vi>/%@.google.gom

Then replace these strings (must be the exactly the same length!):

http://%@.google.com/%@?q=%@&ie=UTF-8&oe=UTF-8
http://%@@search.yahoo.com/%@?p=%@&ie=UTF-8&a=

[Update 2014-12-25] NB. Since GateKeeper was introduced, you can no longer hack the binary. Safari would no longer be allowed to launch.