Skip to content

Customer Support

Why do companies need to make reporting a bug difficult?

I started up a bunch of apps this afternoon, trying to use up a bunch of memory and trigger a bug where something sends a stop signal to Preview. This is a problem because whatever sent the signal never tells it to continue, and this blocks the Dock from being able to activate the app. And this can stop Kaleidoscope’s git diffing from working. (The more common reason for Kaleidoscope’s git diff failing is when the Dock lies about the status of an app. Either an app is running, but the Dock doesn’t think it is, or it isn’t running, but the Dock pretends it is.) And when Kaleidoscope stops working, I stop working until I fix it.

So I reported the bug to Apple on 2011-09-28. And they got back to be requesting more information the next time this happens. Great!

Also please capture a sysdiagnose when the problem occurs by pressing cmd-option-ctrl-shift-period. The resultant diagnostic file will be placed in /var/tmp.

Interesting, I didn’t know about that.

I haven’t run into that bug in a while. Of course, I’m running 10.7.2 now, so _maybe_ it’s fixed. But I thought I’d try to trigger it again, and that’s why I started up all those apps.

And I’m working away, and I notice one core is using 100% CPU. Hmm, what is it? SketchBookExpress. I sample it, and then investigate. It turns out to happen while waiting on the “Welcome to SketchBook Express” panel. Great, I’ll fire off an email to support to let them know about the bug and then I can get back to work.

Except they don’t have a support email address. They use a website, http://getsatisfaction.com/sketchbook, for their support. Fine. “Report a problem. We’ll look for solutions.” Sure, sounds good. Here’s what they need to know:

SketchBook Express 5.2.2
2011 09 04 07 15 (Build 382753)
Mac OS X 10.7.2 (11C74)

App uses 100% CPU on one core while waiting on the “Welcome to SketchBook Express” panel.

Continue. It’s searches the site for similar problems. No, those aren’t the issue. “Nope. Finish posting my question”. Uhm, ok, but I don’t really want to post it, just let them know about the problem. But, whatever, an anonymous post will work. Fine.

“Give your topic a great title”. Ok: “Excessive cpu usage while waiting on the “Welcome to SketchBook Express” panel”. “How does this make you feel?” Oh, goodie, I can add a smilie. No, thanks, actually I just wanted to report the bug. “Get emotional! Describe how this topic makes you feel.” No, really, just trying to report a bug here. “Pick a face to let everybody know how you feel.” Getting angry, but really just want to report the bug.

Ok, almost done. Let’s post. Posting… “Login using any of these options:”. Get Satisfaction — that’s the website, but I don’t want to sign up for yet another account on a website that I’m not going to use. Facebook, don’t use it. Google, got that, but I really don’t want to log in with anything. Fire and forget, let me email the bug report.

Nevermind. I guess I won’t report that bug after all.

class-dump 3.3.4 is now available

Lion’s been out for about six weeks now, so it’s time to release an updated version of class-dump. Especially when the old version gets assertion failures on Lion! So here it is, version 3.3.4. You can download this version from the class-dump project page.

class-dump now handles two more types:

  • _Complex, which fixes a parse error on Grapher.app that has been a problem for a long time
  • long double, encoded as ‘D’; this is only used in a couple of frameworks/apps

And three more load commands:

  • LC_DYLD_ENVIRONMENT, LC_VERSION_MIN_MACOSX, and LC_VERSION_MIN_IPHONEOS.

You can see the rest of the changes in 3.3.4 on that page.

This version is built as a 64-bit Intel executable, supporting 10.6 or later. (10.6 means no more PPC support, and there’s not really any reason to support a 32-bit version any more.)

I’ve switched from Mercurial to Git, and moved the source repository to Github. The latest source is now available at the class-dump git repository at github.

As always, you can send bug reports to me at class-dump@codethecode.com, or nygard at gmail.com

class-dump 3.3.1 is now available

Version 3.3.1 adds support for dumping 10.6 style protected binaries, so that you can once again look at the Finder, Dock, SystemUIServer, and a few other applications. This release fixes a crasher when trying to dump 64-bit files with the 32-bit version of class-dump. Now it prints an error message saying that this isn’t supported. Finally, it generates typedefs for template types that are used in methods, to make the method declarations shorter and more readable.

You can download this version from the class-dump project page. It is built for Mac OS X 10.5 or later.

As always, you can send bug reports to me at class-dump@codethecode.com, or nygard at gmail.com

class-dump 3.3 is now available

A new version of class-dump is now available. You can download it from the links on the class-dump project page. It is built for Mac OS X 10.5 or later.

Version 3.3 adds support for Snow Leopard, improves property handling, improves structure/union handling, fixes a bunch of bugs (including two crashers), and no doubt adds some new bugs.

Snow Leopard

There is a new load command (LC_DYLD_INFO) that contains a byte stream of information that dyld needs to load an image. This replaces the relocation info from the old dynamic symbol table; otool -rv outputs nothing for files built for 10.6. class-dump needs to understand this to correctly look up references to external class names. Version 3.2 shows a blank name for many superclass and category class names. This is fixed in version 3.3.

Snow Leopard also adds a new version of protected segments. These are recognized so that class-dump doesn’t spew out a bunch of garbage. Encrypted iPhone apps (indicated by the LC_ENCRYPTION_INFO command) are recognized for the same reason.

Bug Fixes

There are a lot of bug fixes. First of all, Mac OS X prefers 64-bit executables over 32-bit executables, so there was no need for a separate 32-bit executable. Now there is just one 32/64-bit universal executable. I’ve improved the parsing of C++ types, and class-dump can now make it through Safari and WebKit without complaint. Also fixed a crasher when trying to dump the 10.5 Finder.

Properties

More property attributes are now supported: dynamic, weak, strong, and non-atomic. Thanks to Joe Ranieri for sending a patch to handle these. Objective-C 2.0 categories and protocols now show their properties. Properties are now emitted when the first accessor method is encountered, and those accessors are not shown. Any remaining properties (usually dynamic ones, but there seem to be other cases where this can happen) are shown at the end of the method list. Properties declared in the optional section of protocols appear there in the output. The result is shorter, more readable output.

Property type strings can contain commas (for example, T^{map<int, int>=…},V_task), so we can’t just split the property attribute string on commas. Now class-dump parses the type, and then splits the rest.

Structure Handling

This is the process of trying to make sense out of all the structure and union definitions found in the files, and then to present it in a meaningful way.

Merging member names and types: Not all occurrences of a structure have member names, so it needs to merge member names when possible. For example, there might be both {_NSPoint=”x”f”y”f} and {_NSPoint=ff}. Of course, there are exceptions where structures with the same name have either the same underlying type with different member names, or a different underlying type altogether. An example of this is ^{?=@ii{_flags=b1b1b1b1b12}} from @category NSApplication (NSWindowCache) and ^{__NSRowHeightBucket=fi{_flags=”equalRowSizes”b1″reserved”b31}} from the _NSTableRowHeightStorage class. Anonymous structures can collide in the same way. It is common to have a structure of bitfield flags, and these can easily have the same layout but different member names. Types in methods never have member names, but worse still is that they reduce object types down to ids. That is, @”NSObject” appears as @ instead. Sometimes the compiler generates different names (such as $_2531) for multiple instances of the same union.

Determining how named and anonymous structures are declared: Named structures are generally declared at the top, unless they are only referenced once in an ivar or another structure, or if they are a special case (such as _flags), in which case they get expanded at the point where they occur. Any top level anonymous structures that are referenced by a method, or referenced from more than one place, need to be typedef’d and referred to by that name. And there are a few more special cases that should be handled.

Hmm, when I put it that way it sounds easy enough… But my specification was more like “Take a bunch of types, mix ‘em all together, and generate something pretty”. So there’s a lot of details and special cases to get right, and I ran into them one at a time. When I wrote that code, years ago, it took a lot of time to get it working on all of my test cases. I was tired of tweaking it, happy that it mostly worked, and ready to move on to something else. So I didn’t document it, and now I fear that code. I’ve made a couple attempts to figure out what it was doing and document it, but they didn’t get far.

I was working on the code after 3.2 was released. I decided to cache the parsed types, and pass the parsed types to the type formatter instead of passing a type string. Easy enough, but it introduced a bug, and that led me to the code that I feared. I ended up removing most of the old code and writing it from scratch. By the time is was mostly working again I was tired of tweaking it and ready to move on to something else. But this time I’ve documented it, so that I can remember how it works next year. Or next week.

What’s the result of that effort? Some structures get an actual object type, instead of just an id. Some structures pick up the correct member names, which were missing previously. Some structures have member names generated where they weren’t before. Some of the special cases (like _flag) are handled better.

I have also changed how typedef names are generated. Previously I just added an index to the name. This worked, but was extremely fragile. Unrelated changes to the code could alter the order that structures were processed, and change their name. Updates to frameworks could lead to large diffs in the output. The author of class-dump-z, kennytm, rightly criticized this (and other things!). Now I generate a hash based on the type string (after it’s been filled out with member names, and before the generic _field names have been added), and use that to create a unique typedef name. The results are great. There were only 6 diffs between the 10.5.5 and 10.5.8 AppKits with the new version, compared to 78 with the old.

The named structures are sorted by name and printed first, followed by the special cases. The anonymous structures are next, sorted by nested structure depth and then by the type string, followed by the special cases. #pragma marks are added before each section, and a #pragma mark – before each mach-o file, to make it easy to search for the next section.

As always, you can send bug reports to me at class-dump@codethecode.com. There were a lot of changes, and I haven’t tested it as thoroughly as I normally do.

class-dump 3.2 is now availabe

A new version of class-dump is now available. You can download it from the links on the class-dump project page. It is built for Mac OS X 10.5 or later.

This release includes support for Objective-C 2.0 on both 32 bit (iPhone) and 64 bit (Mac OS X). It shows class properties and handles all documented property attributes. Unrecognized property attributes are noted in a comment following the property declaration. class-dump also shows optional protocol methods. The Objective-C garbage collection status of each file is included in the output, as either “Unsupported”, “Supported”, or “Required”.

To support 64 bit files, class-dump is built as a 64 bit executable. A 32 bit executable (class-dump-32) is provided for 32 bit systems, but it doesn’t handle 64 bit files.

Internally, class-dump now stores the architectures as a cpu type, cpu subtype pair, instead of a string. This was necessary to handle unknown architectures such as the iPhone. For architectures without a recognized name, you can specify them as <cpu type>:<cpu subtype>, values are hex with an optional “0x” prefix. For example, you can use –arch 0xc:0×6 or simply –arch c:6. The –list-arches option will also list unknown architectures in this format.

If you find class-dump useful, I am now accepting donations to support its development. There is a donation button next to the download link on the project page. Thanks!

As always, you can send bug reports to me at class-dump@codethecode.com.

2009-07-05: I’ve fixed the .tar.bz2 packages. Ever since 3.1, my packaging script has been creating a .tar.gz, then bzip2 compressing the result and calling it a .tar.bz2.

Private API to the rescue

I started using a new method, -[NSView viewWillDraw], in one of my views for performing layout. This is a really handy method. It supports further dirtying of the view and frame changes.

But while I was converting my code I discovered a case where my entire view was being redrawn. This was odd, so I made a note to look at it after my conversion was done. I go to quite some trouble to make sure I only dirty the parts of my view that I need to. When I looked through my logs, I didn’t see any calls to -setNeedsDisplay: or -setNeedsDisplayInRect: that would explain why it was redrawing the whole view.

I was ready to blame -viewWillDraw, so I set out to create a simple test case for it. Fortunately that method isn’t at fault. What’s really happening is that changing the frame causes the entire view to be redisplayed!

This is different from the 10.4 behaviour. The documentation for -[NSView setFrame:] says:

This method, in setting the frame rectangle, repositions and resizes the receiver within the coordinate system of its superview. It neither redisplays the receiver nor marks it as needing display. You must do this yourself with display or setNeedsDisplay:.

I ran my test app on 10.4, and it did what I expected. But it didn’t explain what was causing this effect, and how to fix it.

I was reminded of instrumentObjcMessageSends() by a post on Dave Dribin’s blog: Tracing Objective-C messages. So I enabled logging just before I changed the frame, ran the test on 10.5 and 10.4, and compared the results.

There are several changes. In the 10.5 run I see this:


- MyView NSView setFrameSize:
- MyView NSView inLiveResize
- MyView NSView _autoInvalidateBeforeFrameSizeChange
- MyView NSView _setWindowNeedsDisplayInViewsDrawableRect

And a bit later:


- MyView NSView inLiveResize
- MyView NSView _autoInvalidateAfterFrameSizeChange
- MyView NSView _setWindowNeedsDisplayInViewsDrawableRect
- MyView NSView visibleRect

Well, I wonder what _autoInvalidateAfterFrameSizeChange does. First let’s class-dump it and see what classes implement it:


% class-dump -A /System/Library/Frameworks/AppKit.framework -f _autoInvalidate
/*
 *     Generated by class-dump 3.1.2.
 *
 *     class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2007 by Steve Nygard.
 */

@interface NSTextView : NSText <NSTextInput, NSUserInterfaceValidations, NSTextInputClient>
- (void)_autoInvalidateBeforeFrameSizeChange;	// IMP=0x0055d938
- (void)_autoInvalidateAfterFrameSizeChange;	// IMP=0x0055d93c

@interface NSView (NSInternal)
- (void)_autoInvalidateBeforeFrameSizeChange;	// IMP=0x005c504c
- (void)_autoInvalidateAfterFrameSizeChange;	// IMP=0x005c5058

Interesting. I wonder what NSTextView’s implementation of these do.


(gdb) x/20i 0x0055d938
0x55d938 <-[NSTextView _autoInvalidateBeforeFrameSizeChange]>:     blr
0x55d93c <-[NSTextView _autoInvalidateAfterFrameSizeChange]>:      blr
0x55d940 <-[NSTextView setBoundsSize:]>:     mflr    r0

Aha! It doesn’t do anything. I like it. Indeed, testing in both my sample and real applications shows that this fixes the problem.


- (void)_autoInvalidateBeforeFrameSizeChange;
{
    // Do nothing
}

- (void)_autoInvalidateAfterFrameSizeChange;
{
    // Do nothing
}

Sure, Apple doesn’t want us to use private API, but as this illustrates there are cases where they leave us no choice.

By the way, the AppKit release notes say nothing about this new behaviour, not even a hint like there was for the focus ring bleed regions.

I did notice the following in the documentation for -[NSView setFrameSize:]:

In Mac OS X version 10.4 and later, you can override this method to support content preservation during live resizing. In your overridden implementation, include some conditional code to be executed only during a live resize operation. Your code must invalidate any portions of your view that need to be redrawn.

So maybe the bug is mixed up with that. I’ve filed a bug report. In the meantime I’m just happy there’s such an easy workaround.

Using NSTrackingArea for cursor rects

As I said before, Leopard adds a new class, NSTrackingArea, that is now the preferred way of setting up cursor rectangles. It can also handle mouse entered/exited and mouse moved events, but right now I’m only interested in cursor updates.

I didn’t really gain anything from using NSTrackingArea in this case. I still have to clip the rects against the visible area.

I replaced -resetCursorRects with -updateTrackingAreas. The old tracking areas aren’t discarded before this is called, so I have to remove the old ones first. I store the cursor in the NSTrackingArea userInfo so that I can get it in -cursorUpdate:.


- (void)updateTrackingAreas;
{
    NSRect visibleRect;
    NSTrackingArea *trackingArea;

    [super updateTrackingAreas];

    for (trackingArea in [self trackingAreas])
        [self removeTrackingArea:trackingArea];

    visibleRect = [self visibleRect];

    for (STTableColumn *tableColumn in [self tableColumns]) {
        NSRect cursorRect;

        cursorRect = NSIntersectionRect([self resizeCursorFrameForTableColumn:tableColumn], visibleRect);
        if (NSIsEmptyRect(cursorRect) == NO) {
            NSCursor *cursor;

            cursor = [tableColumn resizeCursor];
            if (cursor != nil) {
                trackingArea = [[NSTrackingArea alloc] initWithRect:cursorRect
                                                       options:(NSTrackingCursorUpdate|NSTrackingActiveInKeyWindow)
                                                       owner:self
                                                       userInfo:[NSDictionary dictionaryWithObject:cursor forKey:@"cursor"]];
                [self addTrackingArea:trackingArea];
                [trackingArea release];
            }
        }
    }
}

I’ve removed the -disableCursorRects/-enableCursorRects, and it seems to work fine without a replacement. If I did need to disable them, since I control cursor updates in -cursorUpdate: I could just set a flag and not update them when the flag is set.

I call -updateTrackingAreas directly instead of using -[NSWindow invalidateCursorRectsForView:].

Finally, here’s my -cursorUpdate: method.


- (void)cursorUpdate:(NSEvent *)event;
{
    NSPoint hitPoint;
    NSTrackingArea *trackingArea;
    NSCursor *cursor;

    trackingArea = [event trackingArea];
    cursor = [[trackingArea userInfo] objectForKey:@"cursor"];

    hitPoint = [self convertPoint:[event locationInWindow] fromView:nil];
    if (cursor != nil && [self mouse:hitPoint inRect:[trackingArea rect]]) {
        [cursor set];
    } else {
        [[NSCursor arrowCursor] set];
    }
}

It’s important to use -[NSView mouse:inRect:] instead of NSPointInRect(), otherwise slowly moving the cursor across the top or bottom edge of the tracking area won’t properly update the cursor. And if the tracking area doesn’t cover the entire view, you also get a -cursorUpdate: when the mouse leaves a tracking area, so you need to watch for this case and use a default cursor.

I think in most cases you want to make sure the tracking areas are clipped to the visible rect. The -cursorUpdate: is only called at the transition from one tracking area to another, and it gets sent down the responder chain starting at the view under the mouse. So if you have a tracking area that extends above the top edge of a view, your view won’t get a cursor update when you cross the top edge of the tracking area, and so you won’t change the cursor. You don’t get another update when the mouse enters your view, because it’s still in the same tracking area.

This is slightly different from the old way, because NSWindow was setting the cursor for you. But off the top of my head I can’t imagine a case where you would want the tracking area to extend outside of the view, so this shouldn’t be a problem. Just something to keep in mind.

That’s it, there really wasn’t much to change.

Old style cursor rects

Leopard adds a new class, NSTrackingArea, that is now the preferred way of setting up cursor rectangles. I’m looking at switching some old code to use the new method, but first I’ll describe how it currently works.

The old way

Your NSView subclass implements -resetCursorRects to add the cursor rects. The old ones have already been removed, so you just need to use -[NSView addCursorRect:cursor:] to add the new ones. When the view changes in a way that makes them invalid, such as changing the frame, the cursor rects are invalidated and -resetCursorRects will be called again to set them up. If you make other changes to your view that affect the position of the cursor rects, you can use -[NSWindow invalidateCursorRectsForView:] to invalidate them.

There are two things you need to be careful with. First, you should intersect the cursor rects with the visible rect before adding them, since you won’t get mouse events in these hidden areas and it’s misleading to change the cursor for them. Second, if you change the frame in a modal event loop, the cursors will be reset for each change and the cursor will flicker oddly. You can disable cursor rect updates by calling -[NSWindow disableCursorRects] before your loop, and enable them at the end with -[NSWindow enableCursorRects].

An example

I’ll use a custom table header view as an example. It needs to show resize cursors at the right edge of the columns. This is used in a custom table view, not subclassed from NSTableView, so while the classes correspond to their Cocoa counterparts, they don’t inherit any behavior from them.

The table columns have a minimum and maximum size, and so they need to show different cursors based upon whether they can resize left, right, or in both directions.


@implementation STTableColumn

- (NSCursor *)resizeCursor;
{
    BOOL canShrink, canGrow;

    canShrink = width > minimumWidth;
    canGrow = width < maximumWidth;
    if (canShrink && canGrow) {
        return [NSCursor resizeLeftRightCursor];
    } else if (canShrink) {
        return [NSCursor resizeLeftCursor];
    } else if (canGrow) {
        return [NSCursor resizeRightCursor];
    }

    return nil;
}

@end

The following method clips the proposed cursor rect against the visible rect.


@interface STTableHeaderView

- (void)resetCursorRects;
{
    NSRect visibleRect;

    visibleRect = [self visibleRect];

    for (STTableColumn *tableColumn in [self tableColumns]) {
        NSRect cursorRect;

        cursorRect = NSIntersectionRect([self resizeCursorFrameForTableColumn:tableColumn], visibleRect);
        if (NSIsEmptyRect(cursorRect) == NO) {
	    NSCursor *cursor;

	    cursor = [tableColumn resizeCursor];
	    if (cursor != nil)
                [self addCursorRect:cursorRect cursor:cursor];
        }
    }
}

While editing a column title, the field editor covers the title and so we need to make sure the cursor rect doesn't overlap the field editor.


- (NSRect)resizeCursorFrameForTableColumn:(STTableColumn *)aTableColumn;
{
    NSRect rect;

    if (aTableColumn == nil)
        return NSZeroRect;

    rect = [self bounds];
    if ([aTableColumn index] == editingColumnIndex) {
        rect.size.width = 2;
        rect.origin.x = [aTableColumn maxXPosition];
    } else if ([aTableColumn index] + 1 == editingColumnIndex) {
        rect.size.width = 3;
        rect.origin.x = [aTableColumn maxXPosition] - 3;
    } else {
        rect.size.width = 5;
        rect.origin.x = [aTableColumn maxXPosition] - 3;
    }

    return rect;
}

Changing a column's width changes the frame, so we don't need to invalidate cursor rects in that case. But during the modal event loop we do disable cursor updates to avoid the cursor flickering (both the frame change and auto-scrolling would otherwise reset the cursor). When we do change the width, we manually reset the cursor. Here are the interesting parts:


- (void)startResizingColumn:(NSEvent *)mouseDownEvent;
{
    ...
    [[self window] disableCursorRects];

    while (1) {
        NSEvent *event;
        event = [NSApp nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask | NSPeriodicMask)
                       untilDate:[NSDate distantFuture]
                       inMode:NSEventTrackingRunLoopMode
                       dequeue:YES];

        ...

        if ([event type] == NSLeftMouseDragged) {
            ...
            if ([tableColumn width] != newWidth) {
                [tableColumn setWidth:newWidth];
                [self autoscroll:event];

                // Make sure the cursor changes as we hit min/max widths
                [[tableColumn resizeCursor] set];
            }
        }
        ...
    }

    [[self window] enableCursorRects];
}

When columns are reordered, the frame doesn't change so we do need to invalidate them.

And that's all there is to it.

iCal 3: Shake, Rattle, and Scroll

It’s not my fault. I just wanted to move the iCal window out of the way so that I could see Pierre Igot’s blog entry on iCal 3 hyphenation. I was going to add an appointment and watch the problem in action. But when I moved the window, the day view started scrolling up.

This works best if you have a fast mouse acceleration. Go into the day view, and scroll it to the bottom. Click in the toolbar area above the day view. Try to get close to the day view while still being able to drag the window around. Now, while you’re dragging the window, shake it down and then up again. If you’re fast enough the day view will start scrolling up.

Leopard hates custom views that draw focus rings

The Cocoa release notes say this:

To help guarantee correct redraw of focus rings, AppKit may automatically draw additional parts of a window beyond those that application code marked as needing display. It may do this for the first responder view in the application’s key window, if that first responder view’s focusRingType property is set to a value other than NSFocusRingTypeNone. Any view that can become first responder, but doesn’t draw a focus ring, should have its focusRingType set to NSFocusRingTypeNone to avoid unnecessary additional redraw.

What this means is that when your view calls NSSetFocusRingStyle(NSFocusRingOnly), this ends up calling -[NSWindow _setLastFocusRingView:bleedRegion:], with this view as the first argument. From that point forward, until another view becomes first responder, the entire view will be redrawn any time part of it becomes dirty. This effectively kills your drawing performance.

Setting the view’s focusRingType to NSFocusRingTypeNone has no effect.

There are a few private methods that AppKit is using here.


@interface NSView (NSPrivateFocusRingSupport)
- (id)_focusRingBleedRegion;
- (id)_focusRingClipAncestor;
@end

@interface NSView : NSResponder <NSAnimatablePropertyContainer>
- (NSRect)_focusRingVisibleRect;
@end

NSView’s implementation of _focusRingBleedRegion roughly does this:

  • Converts the visible rect to window base coordinates, scales it, and then insets it be -3 pixels on each side.
  • Calls -_focusRingClipAncestor, and converts that view’s _focusRingVisibleRect to window base coordinates.
  • Creates an NSRegion out of the intersection of two of these rectangles (not exactly sure which two).
  • Insets a rectangle and subtracts that from the region.
  • Returns this region.

The subclasses _NSBroswerScrollView, NSButton, and NSTableView, each implement this method, presumably because they need to. But if they need to, isn’t it likely that other views might need to as well? Except that it’s private, and uses an entirely private class, NSRegion.

Fortunately my workaround doesn’t need to use this method. A little experimentation showed that the focus view and region can be nil. So I just need to override -[NSWindow setLastFocusView:bleedRegion:] and pass on nil values for my particular view:


@implementation NSView (STExtensions)

// This is used on 10.5 by my NSWindow category to work around a problem with focus ring bleed regions.  The default is YES,
// meaning just do the normal behavior.  Subclasses can return NO to disable AppKit's automatic bleed region dirtying for them.
- (BOOL)canControlFocusRingBleedRegion;
{
    return YES;
}

@end

@implementation NSWindow (STExtensions)

+ (void)load;
{
    if (self == [NSWindow class]) {
        Method originalMethod, replacementMethod;

        originalMethod = class_getInstanceMethod(self, @selector(_setLastFocusRingView:bleedRegion:));
        replacementMethod = class_getInstanceMethod(self, @selector(replacement_setLastFocusRingView:bleedRegion:));
        method_exchangeImplementations(originalMethod, replacementMethod);
    }
}

- (void)replacement_setLastFocusRingView:(id)view bleedRegion:(id)region;
{
    if ([view canControlFocusRingBleedRegion]) {
        [self replacement_setLastFocusRingView:view bleedRegion:region]; // Not really recursive.
    } else {
        // We can't control it, so disable it.
        [self replacement_setLastFocusRingView:nil bleedRegion:nil]; // Not really recursive.
    }
}

Then my view just has to return NO from -canControlFocusRingBleedRegion. This works like a charm, assuming your view draws it’s focus ring correctly in all cases, which mine does.

Here’s what the Cocoa release notes have to say about private methods:

The last point (Increased restricted visibility of private symbols in the runtime) is especially important, since it would prevent applications using these symbols from launching in future releases. As always: Please do not access undeclared APIs in your applications. This includes undeclared classes, methods, and instance variables. If you have a strong need for a private API and have no workaround, please let us know.

This is a perfect example of Apple leaving us no choice but to use private API. I’ve filed a bug report about this case.