4

I have a bar-style NSProgressIndicator in a subview of a layer-backed view. Its behavior is sort of complicated, but at certain points, it is displayed as a bar-style indeterminate progress indicator. The problem is, when in this state, it doesn't animate (i.e. twirl the barber pole). Turning off layer backing fixes the issue, but that makes other animations the window does less smooth, so I'm hoping for something better.

Here's the full behavior: when what amounts to a dirty flag is set, it should become visible as an indeterminate, animating progress indicator; then after a short delay (to make sure the user has finished typing) it transforms into a determinate progress indicator and fills as various operations are performed; and finally, at the end of the whole process, it hides itself once more.

To implement this, I've set up the following bindings:

  • Hidden is bound to my model's loading property with an NSNegateBoolean value transformer.
  • Is Indeterminate is bound to my model's waitingForInput property.
  • Value is bound to my model's currentProgress property (which is 0 when waitingForInput is true).
  • Max Value is bound to my model's maximumProgress property (which is 0 when waitingForInput is true).

This mostly works, but with one exception: when waitingForInput is YES, and thus the progress indicator is indeterminate, the progress indicator doesn't animate .

The usual reason for a progress indicator to not update is that the programmer is blocking the run loop with a long-running operation, but I'm not doing that: during the period in question, the run loop is totally open, with just a timer waiting to fire. As far as I know, it's not in some odd mode, either. The app accepts keystrokes and other events during this time without any issues. (The later phase, with a determinate progress indicator filling up, is driven by an asynchronous NSURLConnection, so it's not blocking either.)

I've taken several steps to try to fix this problem:

  • I've tried setting the Animate binding on the progress indicator to my model's waitingForInput property, like Is Indeterminate. This causes the animation to update jerkily when change notifications fire on waitingForInput (waitingForInput happens to send KVO notifications every time the input delay restarts), but I'm hoping for a much smoother animation than that.
  • I've tried using KVO to observe changes to both loading and waitingForInput. When a change is observed, it calls the progress indicator's -startAnimation: and -stopAnimation: methods as appropriate. These have no apparent effect.
  • I've tried setting usesThreadedAnimation on the progress indicator to NO. (A hit on Google suggested this might help with updating problems on layer-backed progress indicators.) This has no apparent effect. I also tried YES, just for kicks, which proved equally futile.

Finally, I've also tried turning off layer backing. This does fix the problem when combined with the Animate binding. However, it degrades the performance of other animations unacceptably, so I'd prefer to avoid doing this.

So, any ideas, anyone? I'd really appreciate some help with this problem!

Becca Royal-Gordon
  • 17,541
  • 7
  • 56
  • 91
  • Thanks for the additional info on the `animate:`/`displayIfNeeded` dance. I guess I needed both because I was trying them from the gdb-console first and forgot to cross-check from the timer... Did you file a bug with Big Fruit yet and if so: what's the rdar so I can refer to it? Also: the animation I achieved was a little stuttery at times. Did you see this too? – danyowdee Feb 05 '11 at 16:33

1 Answers1

5

There is no solution that doesn't require you to either...
a) fumble with the internals of NSProgressIndicator or
b) Roll Your Own™.

So I'd say you should file a bug.

At least on OS X 10.6.5 and above, as soon as you set an indetermined-progress-indicator's wantsLayer property to YES, the animation stops immediately — you can check that for yourself with a reduced test-app (code below).

There was a method called animate: (deprecated since 10.5) which you could repeatedly call on NSProgressIndicator, which may help you (see Using Indeterminate Progress Indicators).

Edit:
Calling animate: followed by displayIfNeeded(Edit 2: as Brent noted, this is redundant) from a timer still works. The "may" simply meant that I don't know if use of deprecated APIs is sanctioned in the App Store or if this matters to you at all.


Sample App

Simple Cocoa app with one controller:

@interface ProgressTester : NSObject {
        NSProgressIndicator *indicator;
}
@property (nonatomic, assign) IBOutlet NSProgressIndicator *indicator;
@property (nonatomic, assign, getter=isLayered) BOOL layered;

- (IBAction)toggleWantsLayer:(id)sender;
@end

@implementation ProgressTester
@synthesize indicator;
@dynamic layered;

- (BOOL)isLayered
{
        return [indicator wantsLayer];
}
- (void)setLayered:(BOOL)wantsLayer
{
        static NSString *layeredKey = @"layered";
        [self willChangeValueForKey:layeredKey];
        [indicator setWantsLayer:wantsLayer];
        [self didChangeValueForKey:layeredKey];
}

- (void)awakeFromNib
{
        // initialize/synchronize UI state
        [self setLayered:NO];
        [indicator startAnimation:self];
}

-(IBAction)toggleWantsLayer:(id)sender
{
        self.layered = ! self.layered;
}
@end

In the NIB:

  1. Instance of the controller
  2. one NSProgressIndicator with style indetermined (connected to the indicator outlet of the controller)
  3. a button with the controller as target and toggleWantsLayer: as action

Added by Brent:

I used the information in this answer to write a simple subclass of NSProgressIndicator:

http://www.pastie.org/1465755 http://www.pastie.org/1540277

Note that in my tests, calling -animate: worked fine without -displayIfNeeded.

Feel free to use it as you see fit. I'd love to hear from you if you use it, though!


Added by Daniel:

A few points about the subclass on pastie:

  1. initWithFrame: should call through to initWithFrame: instead of init (Edit 3: fixed in updated snippet).
  2. The timer need not be retained:
    Scheduling an NSTimer causes the associated runloop to retain and not dispose of it until the timer is invalidated
    (Edit 3: fixed as well).
  3. There is a strong candidate for a retain-cycle with the timer: As an NSTimer retains its target, dealloc will probably never be called if the indicator is released while animated through the timer (I know it's an edge-case but...) (Edit 3: also taken care of).
  4. I'm not entirely sure but think that the implementation of awakeFromNib is redundant, since the KVO setup already happened in initWithFrame: (Edit 3: clarified in the updated snippet).

That said, I'd personally prefer not synthesizing animationTimer and handle the invalidation of the timer in the setter to get rid of the KVO-stuff altogether. (Observing self is a little outside of my comfort zone.)


Added by Anne:

Adding snippet from latest Pastie link for archiving purposes:

ArchProgressIndicator.h

//
//  ArchProgressIndicator.h
//  Translate2
//
//  Created by Brent Royal-Gordon on 1/15/11.
//  Copyright 2011 Architechies. All rights reserved.
//

#import <Cocoa/Cocoa.h>


@interface ArchProgressIndicator : NSProgressIndicator {
@private
    NSTimer * animationTimer;
}

// Just like NSProgressIndicator, but works better in a layer-backed view.

@end

ArchProgressIndicator.m

//
//  ArchProgressIndicator.m
//  Translate2
//
//  Created by Brent Royal-Gordon on 1/15/11.
//  Copyright 2011 Architechies. All rights reserved.
//

#import "ArchProgressIndicator.h"

@interface ArchProgressIndicator ()

@property (assign) NSTimer * animationTimer;

@end

@implementation ArchProgressIndicator

@synthesize animationTimer;

- (void)addObserver {
    [self addObserver:self forKeyPath:@"animationTimer" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:[ArchProgressIndicator class]];
}

- (id)initWithFrame:(NSRect)frameRect {
    if ((self = [super initWithFrame:frameRect])) {
        [self addObserver];
    }
    
    return self;
}

// -initWithFrame: may not be called if created by a nib file
- (void)awakeFromNib {
    [self addObserver];
}

// Documentation lists this as the default for -animationDelay
static const NSTimeInterval ANIMATION_UPDATE_INTERVAL = 5.0/60.0;

- (void)startAnimation:(id)sender {
    [super startAnimation:sender];
    
    if([self layer]) {
        self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:ANIMATION_UPDATE_INTERVAL target:self selector:@selector(animate:) userInfo:nil repeats:YES];
    }
}

- (void)stopAnimation:(id)sender {
    self.animationTimer = nil;
    
    [super stopAnimation:sender];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if(context == [ArchProgressIndicator class]) {
        if([keyPath isEqual:@"animationTimer"]) {
            if([change objectForKey:NSKeyValueChangeOldKey] != [NSNull null] && [change objectForKey:NSKeyValueChangeOldKey] != [change objectForKey:NSKeyValueChangeNewKey]) {
                [[change objectForKey:NSKeyValueChangeOldKey] invalidate];
            }
        }
    }
    else {
        return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    [self removeObserver:self forKeyPath:@"animationTimer"];
    
    [animationTimer invalidate];
    
    [super dealloc];
}

@end
Community
  • 1
  • 1
danyowdee
  • 4,658
  • 2
  • 20
  • 35
  • It's not the answer I wanted to hear, of course, but this is very helpful nonetheless. Thank you very much for looking into this! – Becca Royal-Gordon Jan 15 '11 at 06:09
  • You were right about `[super initWithFrame:]`; it was a thinko that wasn't exposed because I was loading from a nib file (which calls `-initWithCoder:` instead). I usually retain timers out of paranoia, but I think you're right about the retain cycle, so I've changed that. The self-observation is a style thing; I prefer to allow Objective-C to generate atomic accessors for me so I don't have to worry about their details. I've updated the sample code to fix the mistakes, though—thanks! – Becca Royal-Gordon Feb 08 '11 at 10:47
  • It turns out I've made a thinko as well: Not retaining the timer probably doesn't help much. After all, the timer is holding a reference to you until it is invalidated... So depending on what exactly happens, when an NSProgressIndicator gets removed from its superview, you'll likely [leak the indicator and the timer](http://stackoverflow.com/questions/4945028/repeating-nstimer-weak-reference-owning-reference-or-ivar/4946038#4946038) either way. – danyowdee Feb 11 '11 at 14:16
  • This is insane. How can it be so complex to animate a bar!? – Apollo Jun 05 '13 at 10:53
  • Note the date of this answer! IIRC, Lion fixed the problem this code works around. So now it should be as easy as just showing the darn thing. – danyowdee Jun 06 '13 at 21:13