4

NSProgressIndicatorレイヤーバックビューのサブビューにバースタイルがあります。その動作はやや複雑ですが、特定の時点で、バースタイルの不確定な進行状況インジケーターとして表示されます。問題は、この状態ではアニメーション化されない(つまり、理髪店の看板ポールを回転させる)ことです。レイヤーバッキングをオフにすると問題は修正されますが、他のアニメーションではウィンドウのスムーズさが低下するため、より良いものを期待しています。

完全な動作は次のとおりです。ダーティフラグに相当するものが設定されると、不確定なアニメーションの進行状況インジケーターとして表示されるはずです。次に、少し遅れて(ユーザーが入力を終了したことを確認するために)、さまざまな操作が実行されると、確定的な進行状況インジケーターに変換されていっぱいになります。そして最後に、プロセス全体の終わりに、それはもう一度自分自身を隠します。

これを実装するために、次のバインディングを設定しました。

  • Hiddenは、値トランスフォーマーを使用してモデルのloadingプロパティにバインドされます。NSNegateBoolean
  • Is Indeterminateは、私のモデルのwaitingForInputプロパティにバインドされています。
  • はモデルのプロパティにバインドされcurrentProgressます(trueの場合は0waitingForInputです)。
  • 最大値はモデルのプロパティにバインドされmaximumProgressます(trueの場合は0waitingForInputです)。

これはほとんど機能しますが、1つの例外を除いて、がの場合waitingForInputYESしたがって進行状況インジケーターが不確定である場合、進行状況インジケーターはアニメーション化されません。

進行状況インジケーターが更新されない通常の理由は、プログラマーが長時間実行操作で実行ループをブロックしていることですが、私はそれを行っていません。問題の期間中、実行ループは完全に開いており、起動を待機しているタイマー。私の知る限り、それは奇妙なモードでもありません。アプリは、この時間中に問題なくキーストロークやその他のイベントを受け入れます。(確定的な進行状況インジケーターがいっぱいになる後のフェーズは、非同期によって駆動されるNSURLConnectionため、どちらもブロックされません。)

この問題を解決するために、いくつかの手順を実行しました。

  • 進行状況インジケーターのAnimateバインディングを、IsIndeterminatewaitingForInputのようにモデルのプロパティに設定してみました。これにより、変更通知が発生したときにアニメーションがぎくしゃくして更新されます(入力遅延が再開するたびにKVO通知が送信されます)が、それよりもはるかにスムーズなアニメーションを望んでいます。waitingForInputwaitingForInput
  • KVOを使用して、loadingとの両方の変化を観察してみましwaitingForInputた。変更が観察されると、必要に応じて進行状況インジケーター-startAnimation:-stopAnimation:メソッドが呼び出されます。これらには明らかな影響はありません。
  • usesThreadedAnimation進行状況インジケーターをに設定してみましたNO。(Googleでのヒットは、これがレイヤーに裏打ちされた進行状況インジケーターの問題の更新に役立つ可能性があることを示唆しています。)これには明らかな影響はありません。私はまた、キックのためだけに試しYESましたが、それは同様に無駄であることがわかりました。

最後に、レイヤーバッキングをオフにしてみました。これにより、Animateバインディングと組み合わせた場合の問題が修正されます。ただし、他のアニメーションのパフォーマンスが許容できないほど低下するため、これは避けたいと思います。

それで、何かアイデアはありますか?この問題について助けていただければ幸いです。

4

1 に答える 1

5

どちらかを必要としない解決策はありません...
a)の内部をいじくり回すNSProgressIndicatorか、
b)RollYourOwn™。

だから私はあなたがバグを提出するべきだと思います。

少なくともOSX10.6.5以降では、indetermined-progress-indicatorのwantsLayerプロパティをに設定するとYESすぐに、アニメーションがすぐに停止します。これは、縮小されたテストアプリ(以下のコード)で確認できます。

animate:繰り返し呼び出すことができる(10.5以降非推奨)と呼ばれるメソッドがありましたNSProgressIndicator。これ役立つ場合があります(不確定な進行状況インジケーターの使用を参照)。

編集:
呼び出しanimate: に続くdisplayIfNeeded編集2:ブレントが指摘したように、これは冗長です)タイマーからはまだ機能します。「かもしれない」とは、非推奨のAPIの使用がApp Storeで認可されているかどうか、またはこれがあなたにとってまったく重要かどうかがわからないことを意味します。


サンプルアプリ

1つのコントローラーを備えたシンプルなCocoaアプリ:

@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

NIBの場合:

  1. コントローラのインスタンス
  2. スタイルが決定されていない1つのNSProgressIndicator(indicatorコントローラーのアウトレットに接続されている)
  3. toggleWantsLayer:コントローラをターゲットおよびアクションとして持つボタン

ブレントによって追加されました:

この回答の情報を使用して、NSProgressIndicatorの単純なサブクラスを作成しました。

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

私のテストでは、呼び出し-animate:は。なしで正常に機能したことに注意してください-displayIfNeeded

必要に応じて自由に使用してください。でも、使っていただけたら、ぜひお聞かせください!


ダニエルによって追加されました:

パスティのサブクラスに関するいくつかのポイント:

  1. initWithFrame:initWithFrame:の代わりにに電話する必要がありますinit編集3:更新されたスニペットで修正されました)。
  2. タイマーを保持する必要はありません。
    スケジュールを設定するNSTimerと、関連するrunloopが実行されretain、タイマーがinvalidatedになるまで破棄されません。
    編集3:同様に修正されました)。
  3. タイマーを使用した保持サイクルの有力な候補があります。NSTimer保持はターゲットを保持するため、タイマーを介してアニメーション化されているときにインジケーターが解放された場合、deallocはおそらく呼び出されません(エッジケースであることはわかっていますが...)編集3:面倒も見てくれます)。
  4. 完全にはわかりませんがawakeFromNib、KVOのセットアップはすでに行われているため、の実装は冗長だと思います。initWithFrame:編集3:更新されたスニペットで明確にされています)。

とは言うものの、私は個人的animationTimerには、KVOのものを完全に取り除くために、セッターでタイマーの無効化を合成して処理しないことを好みます。(観察selfは私の快適ゾーンの少し外にあります。)


アンによって追加されました:

アーカイブ目的で最新のPastieリンクからスニペットを追加する:

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
于 2011-01-12T10:52:54.790 に答える