Controlling How NSThread and NSRunLoop Exit

While most concurrency can be dealt with by using GCD dispatch queues, there are some things for which you will want to use a separate NSThread. For instance, concurrent network programming remains mostly in this area. You may consider using the popular CocoaAsyncSocket library, which abstracts away much of the complexity of socket programming on iOS. But, for the sake of this post, assume some work requires a separate thread, and we desire to cleanly start and stop the thread. That is, when we decide to start or stop the thread, we want to ensure it has been initialized or destroyed before moving on.

You can get the final version of the code example below at github.

Thread Setup

Remember that threads are an artefact of the operating system and not the objective-c runtime. That means all the nice stuff like an NSAutoreleasePool and an NSRunLoop need to be managed by the thread's top level code. Here is a code snippet of a thread's main method that sets up an autorelease pool as well as starts pumping NSRunLoop.

- (void)start
{
  if (_thread) {
    return;
  }

  _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadProc:) object:nil];
}

- (void)threadProc:(id)ignored
{
  @autoreleasepool {
    // Startup code here

    // Just spin in a tight loop running the runloop.
    do {
      [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]
    } while (TRUE);
  }
}

This code has many problems. Note that each run of the runloop has a timeout of 1 second. When the runloop has nothing to do for extended periods of time, the thread will still wake up every second and go through the while loop's code, only to end up sitting in the runloop's wait condition. That burns CPU and battery. Also, this thread has no exit condition. Even if we want the thread to live until the end of the application's life, we probably want a way to shut it down cleanly. Here's an enumeration of the problems we want to fix:

  1. Unsure when the thread is ready for work
  2. Never goes to sleep
  3. No way to exit the thread cleanly

Solving all three problems at once is tricky. Fixing #2 means keeping the runloop asleep, preferrably waking only when there is work to be done (being notified by a runloop source). But, that makes it difficult to exit the thread in a timely manner.

Wait for Infinity

How can we make NSRunLoop wait for infinity? The goal is to make sure that the thread goes to sleep. Looking at the documentation for runUntilDate:, this is not the case:

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate: until the specified expiration date.

The second sentence means this code keeps spinning on the CPU, which is the exact opposite of our desired behavior. A better alternative is runMode:beforeDate:, for which the documentation reads:

If no input sources or timers are attached to the run loop, this method exits immediately and returns NO; otherwise, it returns after either the first input source is processed or limitDate is reached.

At least there's a chance the thread will sleep when using this method. However, the thread still runs in a continuous loop when there are no sources or timers. If you are only using a run loop to dispatch performSelector: calls, you need to add a runloop source of your own to make the thread sleep. As a side effect, it would be effective to use this source to send work to the thread, but that is an exercise left to the reader.

One last thing: what value of NSDate should be used? Any large value is fine, as waking the thread once a day is a large enough to keep the thread silent. +[NSDate distantFuture] is a convenient factory for such a date.

static void DoNothingRunLoopCallback(void *info)
{
}

- (void)threadProc:(id)ignored
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    do {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                               beforeDate:[NSDate distantFuture]];
    } while (TRUE);

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}

The exit condition

Now that the thread sleeps forever, how can we ensure a clean shutdown of the thread? It's entirely possible to use +[NSThread exit] to kill the currently running thread. But, this does not clean up stack references to heap objects (like the runloop source that is created), drain the top level autorelease pool, nor allows the thread to flush any remaining work. Instead, we need to make the runloop stop sleeping in order to exit runMode:beforeDate:, and we need a condition the thread can check in order to know it's time to shut down.

NSRunLoop's conditions for exiting runMode:beforeDate: are somewhat limited. Its return values being YES (when the runloop processed an input source or if the timeout was reached) or NO (when the runloop could not be started) aren't great. Given that the documentation states that NO is returned only when there are no sources or timers, our code will never see NO returned since we added an input source to keep the runloop from spinning!

Luckily, NSRunLoop controls the same object that CFRunLoop APIs do. The CoreFoundation alternative is to use CFRunLoopRunInMode, which provides a more specific reason for the runloop's exit. Specifically, kCFRunLoopRunStopped indicates the runloop was stopped by CFRunLoopStop. This is also the reason that CFRunLoopRun exits (other than having no sources or times, but that will not occur due to our fake source), so we don't need to bother with CFRunLoopRunInMode, nor check a condition.

It is best to execute CFRunLoopStop on the target thread itself. We can do this by using performSelector:onThread:withObject:waitUntilDone:.

The new threadProc: looks like this:

- (void)threadProc:(id)ignored
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}

We can use code like the following to exit the thread quickly from any thread, including the target thread itself:

- (void)stop
{
  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  _thread = nil;
}

- (void)_stop
{
  CFRunLoopStop(CFRunLoopGetCurrent());
}

Synchronizing thread startup and exit

Two more problems to solve: when starting the thread, can we ensure that it is ready? When shutting down a thread, can we ensure it is gone?

I would note that there are better threading patterns than attempting to ensure the state of the target thread. For example, a thread should be able to accept work, but simply not process it until it is ready. Resources outside of the thread's runloop should be minimal such that ensuring the thread is no longer executing should be above and beyond the desired knowledge of the thread's state.

It is tempting to simply add waitUntilDone:YES to the performSelector statement in order to wait until the target thread has exited, but that would only wait until the _stop selector was invoked, and not wait for the thread's cleanup code to be run. In order to do that, we need to make a new assumption: the target thread is being shut down from another thread. It would be impossible for a thread to wait for itself to shut down.

In order for the target thread to signal to the control thread that it is done, a condition must be shared between them. NSCondition provides convenient semantics for our purposes.

The thread management code is below. This pattern keeps a thread with no work asleep for long periods of time, while allowing for a fast and clean exit. It also allows for startup and shutdown to be synchronized.

- (void)start
{
  if (_thread) {
    return;
  }

  _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadProc:) object:nil];

  // _condition was created in -init
  [_condition lock];
  [_thread start];
  [_condition wait];
  [_condition unlock];
}

- (void)stop
{
  if (!_thread) {
    return;
  }

  [_condition lock];
  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  [_condition wait];
  [_condition unlock];
  _thread = nil;
}

- (void)threadProc:(id)object
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);

    [_condition lock];
    [_condition signal];
    [_condition unlock];
  }
}

Ensuring Destruction of Thread Resources

There is still another issue with this code: when the thread has signaled its exit, the autorelease pool has not yet been drained. Without ensuring that the thread's memory resources have been released, the purpose of synchronizing the thread's exit becomes much less appealing.

There's a bit of a catch-22. NSCondition makes no promise that it is free from using -autorelease in its implementations of -lock, -signal, and -unlock. That means there should be a valid NSAutoreleasePool when using these APIs. We have two solutions available to us. We can either manually drain the autorelease pool, or use a different way to synchronize the thread's exit that waits until threadProc: has exited. The first is somewhat messy. The second has two variants of its own.

Manual Autorelease

In order to use NSAutoreleasePool directly, you must disable ARC.

Remember that -[NSAutoreleasePool drain] is effectively the same as -[NSAutoreleasePool release] and that the pool is no longer valid after draining it. So, manually draining an autorelease pool means creating another one to ensure the NSCondition APIs have the right environment.

- (void)threadProc:(id)object
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);

    // Release all accumulated resources, but make sure NSCondition has the
    // right environment.
    [pool drain];
    pool = [[NSAutoreleasePool alloc] init];

    [_condition lock];
    [_condition signal];
    [_condition unlock];
  }
  [pool drain];
}

Using NSThreadWillExitNotification

NSThreadWillExitNotification is a notification sent by NSThread when the thread's main function has finished and the thread is about to finish execution. This must happen after threadProc:, so this ensures the thread's top level autorelease pool has been drained. Since the notification fires on the exiting thread, NSCondition is still used to synchronize the state of the thread.

- (void)stop
{
  if (!_thread) {
    return;
  }

  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc addObserver:self selector:@(_signal) name:NSThreadWillExitNotification object:_thread];

  [_condition lock];
  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  [_condition wait];
  [_condition unlock];

  [nc removeObserver:self name:NSThreadWillExitNotification object:_thread];
  _thread = nil;
}

- (void)threadProc:(id)object
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}

- (void)_signal
{
  [_condition lock];
  [_condition signal];
  [_condition unlock];
}

Using pthreads

All of the above solutions have one slight problem: the thread hasn't quite exited when the control thread believes it has. The target thread is very close to being done, but that is not the same as done.

To ensure that, we must go outside the realm of NSThread and use pthreads instead. pthread_join guarantees the target thread has terminated. Using pthreads makes the code a bit more verbose, and some of the memory management must be done carefully. In particular, self is retained when specifying it as an argument of NSThread's initializer, but will not be retained when using it as an argument to pthread_create. Note that we still need an NSThread reference to use performSelector:onThread:withObject:waitUntilDone:, but there is no way to convert a pthread_t to an NSThread. Luckily, +[NSThread currentThread] can obtain the correct object reference.

NSCondition is still used to synchronize thread startup. Because it is not used for any other purpose, it is not necessary to lock the condition before starting the thread. However, to be consistent with previous code, we will follow the previous pattern of creating the thread in a suspended state and resuming it with the condition's lock held.

static void *ThreadProc(void *arg)
{
  ThreadedComponent *component = (__bridge_transfer ThreadedComponent *)arg;
  [component threadProc:nil];
  return 0;
}

- (void)start
{
  if (_thread) {
    return;
  }

  if (pthread_create_suspended_np(&_pthread, NULL, &ThreadProc, (__bridge_retained void *)self) != 0) {
    return;
  }

  // _condition was created in -init
  [_condition lock];
  mach_port_t mach_thread = pthread_mach_thread_np(_pthread);
  thread_resume(mach_thread);
  [_condition wait];
  [_condition unlock];
}

- (void)stop
{
  if (!_thread) {
    return;
  }

  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  pthread_join(_pthread, NULL);
  _thread = nil;
}

- (void)threadProc:(id)object
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    // Obtain the current NSThread before signaling startup is complete.
    _thread = [NSThread currentThread];

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}

Code Samples

Head on over to github to download code samples for all four working versions of the code above.

Synchronizing GCD Dispatch Queues

Let's consider some use cases of GCD dispatch queues. They are often used in producer-consumer relationships for doing asynchronous work, either in a serial or parallel fashion.

In rare cases, you may need to synchronize the work on your queue with another thread. However, a dispatch queue does not operate on a given thread, and combining synchronization primitives with each block placed on the queue would be complicated and fragile.

A better method is to pause and resume the execution of a dispatch queue using dispatch_suspend and dispatch_resume.

dispatch_suspend is Asynchronous

But, there's something curious about dispatch_suspend. Apple's documentation for the function says:

The suspension occurs after completion of any blocks running at the time of the call.

The documentation doesn't tell us whether dispatch_suspend waits for these blocks to complete before returning, so I created a small test program to explore this. It tests this by queueing a block that waits on a condition that only becomes true if dispatch_suspend is asynchronous. If dispatch_suspend is synchronous, the application will hang.

When running this program on OS X 10.9, the program shows dispatch_suspend is an asynchronous process, so it can't be used on its own to synchronize a thread with work on a dispatch queue directly. However, there is a way we can build on top of it.

Barrier Blocks

Another facility provided by dispatch queues are barrier blocks. Barrier blocks work on both serial and concurrent dispatch queues. When submitted to a queue, all blocks ahead of the barrier will complete before the barrier starts execution, and all blocks behind the barrier will only start executing once the barrier is complete. For serial queues, this is the same as queueing any block. For concurrent queues, this ensures ordering of work and is especially handy for our purposes. Submitting a barrier block can be done with dispatch_barrier_async and dispatch_barrier_sync (and their function variants).

Synchronizing Queues

Armed with this knowledge, we can create a blocking synchronize the work done on a dispatch queue with an external thread like so:

dispatch_barrier_sync(queue, ^{ dispatch_suspend(queue); });

// Do some stuff

dispatch_resume(queue);

Note this doesn't only wait for running blocks to finish; it flushes any pending work. To achieve the former semantics, you would need to use synchronization primitives in each of your dispatch queue blocks (without a higher level abstraction).

Using NSPopover with NSStatusItem

Mac OS X Lion supports a new Cocoa object called NSPopover. This object acts a lot like the popover introduced in CocoaTouch on iPad. There are lots of places normal applications can use NSPopover naturally. I recently started working on a personal project that uses an NSStatusItem, and wanted to use an NSPopover to manage the primary user interface for the content shown when interacting with the NSStatusItem.

Unfortunately, the interactions between a hidden object called NSPopoverWindow and Mac OS X’s NSStatusBarWindow create some problems. On top of this, having an NSPopover where the content view contains edit fields creates further problems.

However, I was finally able to get the interactions working correctly (at the expense of using the built-in NSPopoverBehavior styles). This post takes you through the problems and describes my solutions.

Yes, I could have used the Popup project, but I just wanted to make NSPopover work.

Initial Code

The initial code is pretty simple: just create a custom view and attach it to an NSStatusItem object. Even though I did not need more than the functionality provided by the default NSStatusItem object, NSPopover requires a view for the showRelativeToRect:ofView:preferredEdge: selector. I couldn’t figure out a way to access the _fView member of the NSStatusItem that’s visible in the debugger, so I was stuck re-implementing the basic NSStatusItem view functionality of an image that flips when activated. I call this BRStatusItemIconView. The code in the application delegate looks like this:

NSStatusItem* statusItem = [[NSStatusBar systemStatusBar]
  statusItemWithLength:32];
[statusItem setHighlightMode:YES];

_iconView = [[BRStatusItemIconView alloc]
  initWithStatusItem:statusItem];
_iconView.image = [NSImage imageNamed:@"Status"];
_iconView.highlightedImage = [NSImage imageNamed:@"StatusHighlighted"];

The initWithStatusItem: selector creates the necessary subviews (since it’s not loaded from a nib) and attaches itself to statusItem through setView:.

I used a separate object to be the popover controller and attach itself to the NSStatusItem. Since I used a custom view, the action and target properties of the NSStatusItem couldn’t be used. I created a BRStatusItemIconViewDelegate protocol that had one selector:

- (void)activated:(BRStatusItemIconView*)sender;

When the icon was clicked, it would invoke this selector on a delegate. My BRStatusItemPopoverController object is the popover’s controller and attaches itself to the BRStatusIconView created in the application delegate. The popover was initialized like this:

_popover = [[NSPopover alloc] init];
_popover.behavior = NSPopoverBehaviorApplicationDefined;
_popover.contentViewController = viewController;
_popover.delegate = self;
_popover.animates = NO;

Note I used NSPopoverBehaviorApplicationDefined, the importance of which will be discussed later. The viewController here was a controller for a complex view that includes an edit field, which is also important to note.

So, now all I needed was to open and close the NSPopover! The simple version of the code in BRStatusItemPopoverController looked like this:

- (void)close {
  [_popover close];
  _shown = NO;
}

- (void)open {
  BRStatusItemIconView* view = _statusItem.view;
  [_popover showRelativeToRect:view.bounds ofView:view
    preferredEdge:NSMaxYEdge];
  _shown = YES;
}

- (void)activated:(BRStatusItemIconView*)sender {
  if (_shown) {
    [self close];
  } else {
    [self open];
  }
}

Hit run to try it out, and… it works! However, if the popover’s content view contained an NSTextField, you’d find that the field refuses to become first responder! Even making sure the field was marked editable in Interface Builder didn’t help. #FAIL.

Fixing Edit Fields

According to this StackOverflow question, the problem with the NSTextField is the parent window’s inability to become the key window. The answer in that question is to write a global category for your application:

NSWindow+canBecomeKeyWindow.h
@interface NSWindow (canBecomeKeyWindow)

@end

NSWindow+canBecomeKeyWindow.m
@implementation NSWindow (canBecomeKeyWindow)

//This is to fix a bug with 10.7 where an NSPopover with a text field
//cannot be edited if its parent window won't become key
//The pragma statements disable the corresponding warning for
//overriding an already-implemented method
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (BOOL)canBecomeKeyWindow
{
    return YES;
}
#pragma clang diagnostic pop

@end

Trying this out appears to work when you click the status item. You can click the NSTextField and it becomes editable! But, this has created a subtle problem: the status item only works correctly if the application that owns it is active. When inactive, the status item weirdly requires a double click to activate it, and even then, the double click sends two mouseDown: events to BRStatusItemIconView, causing the interaction to be out of phase with the user’s intention.

Fixing the double click

Long story short, we need to control when the NSStatusBarWindow window, the parent of BRStatusItemIconView, is allowed to become key. When the popover is opened, we want that to happen so that the popover’s content view can become first responder. When the popover is closed, we want to go back to the original behavior so that input to BRStatusItemIconView doesn’t get blocked. But, we aren’t familiar with the original behavior because it’s hidden by the original implementation of canBecomeKeyWindow:. Using a technique called method swizzling that helps us do this. Note that there is an updated version of method swizzling discussed here, but I’ve not implemented it in my code yet. The new code looks like this:

#import "NSWindow_canBecomeKeyWindow.h"
#include

BOOL shouldBecomeKeyWindow;
NSWindow* windowToOverride;

@implementation NSWindow (canBecomeKeyWindow)

//This is to fix a bug with 10.7 where an NSPopover with a text field
//cannot be edited if its parent window won't become key
//The pragma statements disable the corresponding warning for
//overriding an already-implemented method
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (BOOL)popoverCanBecomeKeyWindow
{
    if (self == windowToOverride) {
        return shouldBecomeKeyWindow;
    } else {
        return [self popoverCanBecomeKeyWindow];
    }
}

+ (void)load
{
    method_exchangeImplementations(
      class_getInstanceMethod(self, @selector(canBecomeKeyWindow)),
      class_getInstanceMethod(self, @selector(popoverCanBecomeKeyWindow)));
}
#pragma clang diagnostic pop

@end

Now, shouldBecomeKeyWindow and windowToOverride just need to be set correctly. As I said above, we want windowToOverride to be the NSStatusBarWindow and shouldBecomeKeyWindow to change based on the status of the popover being visible. The new BRStatusItemPopoverController code is this:

- (void)close
{
    [_popover close];
    shouldBecomeKeyWindow = NO;
    _shown = NO;
}

- (void)open
{
    BRStatusItemIconView* view = (BRStatusItemIconView*)_statusItem.view;

    shouldBecomeKeyWindow = YES;
    [_popover showRelativeToRect:view.bounds ofView:view
      preferredEdge:NSMaxYEdge];

    windowToOverride = view.window;
    [view.window becomeKeyWindow];
    _shown = YES;
}

Adding transient behavior

Finally! Things work as they should. However, one cool thing about NSPopover’s default functionality is its ability to be a transient presence for the user. For applications that use the status bar, this is good behavior. However, because NSPopoverBehaviorApplicationDefined was used, we have to duplicate that functionality. Basically, we want the popover to be able to disappear when the application resigns being active. This can be done by listening to the appropriate notifications in BRStatusItemPopoverController and changing the ability of the NSStatusBarWindow to become key, or close the popover.

- (void)applicationDidResignActive:(NSNotification*)n
{
    if ((self.behavior == BRStatusItemPopoverBehaviorTransient) &&
       _shown) {
        [self close];
    } else if (self.behavior == BRStatusItemPopoverBehaviorPermanent) {
        shouldBecomeKeyWindow = NO;
    }
}

Conclusion

Apple has some bugs to fix related to NSPopover and NSStatusBarWindow. It seems obvious to me that applications that live in the status bar should use NSPopover to present complex user interfaces, but the complexity of doing so right now is too high.

Cocoa: Rotation in Scrollable Document Views

I’m continuing work on my mac project, which involves a scalable document that can be embedded in an NSScrollView. I recently wanted to add orientation to the set of valid transformations. While I’m sure there are more generic solutions that involve setting a composite affine transformation on the underlying CALayer, I wanted something that I could implement in an hour or so. I ran into some issues that I thought I’d write about in hopes of saving other people some time.

When coding against the SDK for OS X Lion, NSView has a setFrameCenterRotation: selector, which is very convenient for rotating about the center of the view. If you need to run against older versions of OS X, you can use a combination of setFrameOrigin: and setFrameRotation: to accomplish the same thing. These rotations don’t affect the internal coordinate system of the NSView, but change its frame in the superview – also very convenient when it comes to performing layout in the rotated view.

I experimented with these for my document view’s transformations and found some quirks. Firstly, I tried to simply rotate the NSScrollView’s documentView. From my previous post, this is the HostView object. That didn’t go quite as well as I had hoped: the rotation occurred around the incorrect locus, and the scroll view got very confused about positioning. When I performed the rotation, the document would jump to an odd place, and then when I moved the scroll bars, the document would jump back to the origin.

After that, I attempted to rotate the _docView inside the HostView object. Success! At least, initial success: while the rotation occurred and the document stayed stable inside the scroll view, the dimensions of the HostView object stayed the same and the _docView was positioned incorrectly. I found that the NSViewFrameDidChangeNotification was still being raised. When I analyzed the newly calculated frame for the HostView, I found the width and height were the same as the original, when it should have been changed.

Changing the code in updateFrame: to detect the _docView in landscape orientation and transforming the frame appropriately makes the scroll view recalculate its scroll extents correctly. There’s still the problem of the incorrectly positioned document. I’m not sure why Cocoa does this, but when calculating the frame for a rotation, the view would consistently get shifted by a size proportional to the size of the view. I had to manually adjust the frame after the rotation had adjusted it.

After all is said and done, the new code looks like this.