UIViewに強制的に再描画させる最も強力な方法は何ですか?


117

アイテムのリストを含むUITableViewがあります。アイテムを選択すると、viewControllerがプッシュされ、次の処理に進みます。fromメソッドviewDidLoad私は、サブビュー(drawRectがオーバーライドされたUIViewサブクラス)で必要なデータのURLRequestを発行します。クラウドからデータが到着すると、ビュー階層の構築を開始します。問題のサブクラスにデータが渡され、そのdrawRectメソッドがレンダリングに必要なすべてを備えています。

だが。

drawRectを明示的に呼び出さないため-Cocoa-Touchはそれを処理します-Cocoa-Touchに本当にこのUIViewサブクラスをレンダリングしてほしいと通知する方法はありません。いつ?今はいいだろう!

[myView setNeedsDisplay]を試しました。このようなことは時々うまくいきます。非常にむら。

私は何時間もこれに取り組んできました。UIViewの再レンダリングを強制する堅実で保証されたアプローチを私に提供してくれる誰かができますか?

以下は、ビューにデータをフィードするコードのスニペットです。

// Create the subview
self.chromosomeBlockView = [[[ChromosomeBlockView alloc] initWithFrame:frame] autorelease];

// Set some properties
self.chromosomeBlockView.sequenceString     = self.sequenceString;
self.chromosomeBlockView.nucleotideBases    = self.nucleotideLettersDictionary;

// Insert the view in the view hierarchy
[self.containerView          addSubview:self.chromosomeBlockView];
[self.containerView bringSubviewToFront:self.chromosomeBlockView];

// A vain attempt to convince Cocoa-Touch that this view is worthy of being displayed ;-)
[self.chromosomeBlockView setNeedsDisplay];

乾杯、ダグ

回答:


193

を強制的UIViewに再レンダリングする保証された確実な方法は[myView setNeedsDisplay]です。これに問題がある場合は、次のいずれかの問題が発生している可能性があります。

  • 実際にデータを取得する前に呼び出すか、-drawRect:何かを過剰にキャッシュしています。

  • このメソッドを呼び出す瞬間にビューが描画されることを期待しています。Cocoa描画システムを使用して「今すぐ今すぐ描画」を要求する方法は意図的にありません。これは、ビュー合成システム全体を破壊し、パフォーマンスをゴミ箱に落とし、あらゆる種類のアーティファクトを作成する可能性があります。「これは次のドローサイクルでドローする必要がある」と言う方法は限られています。

必要なものが「いくつかのロジック、描画、いくつかのより多くのロジック」である場合、「いくつかのより多くのロジック」を別のメソッドに配置し、それを-performSelector:withObject:afterDelay:0の遅延で使用して呼び出す必要があります。次の描画サイクル。この種のコードの例と、それが必要になる場合については、この質問を参照してください(コードが複雑になるため、可能であれば他の解決策を探すのが通常最善です)。

物事が描かれていると思わない場合は、ブレークポイントを設定し-drawRect:、いつ呼び出されるかを確認します。を呼び出している-setNeedsDisplay-drawRect:、次のイベントループで呼び出されない場合は、ビュー階層を掘り下げて、どこかを出し抜こうとしていないことを確認してください。過剰な賢さは、私の経験において描画不良の最大の原因です。システムをだまして必要なことを実行する方法を最もよく知っていると思うときは、通常、望んでいないことを正確に実行します。


ロブ、これが私のチェックリストです。1)データはありますか?はい。私はメソッドでビューを作成します-hideSpinner-connectionDidFinishLoadingからメインスレッドで呼び出されます:したがって:[self performSelectorOnMainThread:@selector(hideSpinner)withObject:nil waitUntilDone:NO]; 2)すぐに絵を描く必要はありません。必要なだけです。今日。現在、それは完全にランダムであり、私の制御範囲外です。[myView setNeedsDisplay]は完全に信頼できません。私は、viewDidAppear:で[myView setNeedsDisplay]を呼び出すまで行ってきました。Nuthin '。ココアは単に私を無視します。狂ってる!!
-dugla

1
このアプローチでは、setNeedsDisplayとの実際の呼び出しの間に遅延が生じることがよくありdrawRect:ます。呼び出しの信頼性はここにありますが、これを「最も堅牢な」ソリューションとは呼びません。堅牢な描画ソリューションは、理論上、描画を要求するクライアントコードに戻る直前にすべての描画を実行する必要があります。
Slipp D. Thompson 2013

1
私はあなたの答えについて以下でより詳細にコメントしました。メソッドを呼び出すことにより、描画システムの順序が狂うように強制しようとすることは、ドキュメントが呼び出さないように明示的に言っていることは堅牢ではありません。せいぜい、未定義の動作です。ほとんどの場合、パフォーマンスと描画品質が低下します。最悪の場合、デッドロックまたはクラッシュする可能性があります。
Rob Napier 2013年

1
戻る前に描画する必要がある場合は、イメージコンテキストに描画します(多くの人が期待するキャンバスと同じように機能します)。ただし、合成システムをだまそうとしないでください。最小限のリソースオーバーヘッドで非常に高速に高品質のグラフィックを提供するように設計されています。その一部は、実行ループの最後で描画ステップを合体させることです。
Rob Napier 2013年

1
@RobNapierなぜあなたがそんなに守備的になっているのか分かりません。再度確認してください; API違反はありません(自分のメソッドを呼び出すことは違反ではないと主張するあなたの提案に基づいてクリーンアップされていますが)、デッドロックやクラッシュの可能性もありません。また、これは「トリック」ではありません。通常の方法でCALayerのsetNeedsDisplay / displayIfNeededを使用します。さらに、GLKView / OpenGLと並行した方法でQuartzで描画するために、かなり長い間使用しています。リストしたソリューションよりも安全で安定しており、高速であることが証明されています。私を信じないなら、試してみてください。ここで失うものは何もありません。
Slipp D. Thompson 2013

52

setNeedsDisplayとdrawRect:(5秒)の呼び出しの間に大きな遅延があるという問題がありました。メインスレッドとは別のスレッドでsetNeedsDisplayを呼び出したことがわかりました。この呼び出しをメインスレッドに移動した後、遅延はなくなりました。

これが何らかの助けになることを願っています。


これは間違いなく私のバグでした。メインスレッドで実行しているという印象を受けましたが、NSLogged NSThread.isMainThreadになって初めて、メインスレッドでレイヤーの変更を行わないコーナーケースがあることに気付きました。髪の毛が抜けるのを防いでくれてありがとう!
T氏

14

見返りを強制するための返金保証付きの強化された具体的な方法 同期的に (呼び出しコードに戻る前描画CALayerUIViewサブクラスとの相互作用を構成することです。

UIViewサブクラスでdisplayNow()、レイヤーに「表示用にコースを設定してからそうする」ように

迅速

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
public func displayNow()
{
    let layer = self.layer
    layer.setNeedsDisplay()
    layer.displayIfNeeded()
}

Objective-C

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
- (void)displayNow
{
    CALayer *layer = self.layer;
    [layer setNeedsDisplay];
    [layer displayIfNeeded];
}

またdraw(_: CALayer, in: CGContext)、プライベート/内部描画メソッドを呼び出すメソッドを実装します(これは、UIViewがであるため機能しますCALayerDelegate

迅速

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
override func draw(_ layer: CALayer, in context: CGContext)
{
    UIGraphicsPushContext(context)
    internalDraw(self.bounds)
    UIGraphicsPopContext()
}

Objective-C

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    UIGraphicsPushContext(context);
    [self internalDrawWithRect:self.bounds];
    UIGraphicsPopContext();
}

カスタムを作成します internalDraw(_: CGRect)フェイルセーフとともにメソッドをdraw(_: CGRect)

迅速

/// Internal drawing method; naming's up to you.
func internalDraw(_ rect: CGRect)
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
override func draw(_ rect: CGRect) {
    internalDraw(rect)
}

Objective-C

/// Internal drawing method; naming's up to you.
- (void)internalDrawWithRect:(CGRect)rect
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
- (void)drawRect:(CGRect)rect {
    [self internalDrawWithRect:rect];
}

そして、(コールバックなどからmyView.displayNow()本当に本当にそれを描画する必要があるときはいつでも呼び出してください。私たちのメソッドはto に通知します。これは同期的に私たちにコールバックしてで描画を行い、次に進む前にコンテキストに描画されたものでビジュアルを更新します。CADisplayLinkdisplayNow()CALayerdisplayIfNeeded()draw(_:,in:)internalDraw(_:)


このアプローチは、上記の@RobNapierと似ていますが、displayIfNeeded()に加えて呼び出しを行うという利点があり、setNeedsDisplay()同期が行われます。

これは、 CALayer sがsよりも多くの描画機能を公開しているUIViewレイヤーはビューよりも低レベルであり、レイアウト内で高度に構成可能な描画を目的として明示的に設計されており、(Cocoaの多くのものと同様に)柔軟に使用できるように設計されています(親クラスとして、または委譲者として、または他の描画システムへの橋渡しとして、またはそれら自身で)。CALayerDelegateプロトコルの適切な使用により、これらすべてが可能になります。

の構成可能性CALayerの詳細については、 『コアアニメーションプログラミングガイド』の「レイヤーオブジェクト設定」セクションを参照してください


のドキュメントdrawRect:には「このメソッドを自分で直接呼び出さないでください」と明記されていることに注意してください。また、CALayer displayは「このメソッドを直接呼び出さないでください」と明示的に言っています。レイヤーをcontents直接同期的に描画したい場合は、これらのルールに違反する必要はありません。contentsいつでも(バックグラウンドスレッドでも)レイヤーの描画を行うことができます。そのためにビューにサブレイヤーを追加するだけです。しかし、それは画面に表示するのとは異なります。画面は正しい合成時間まで待機する必要があります。
Rob Napier 2013年

ドキュメントはUIViewのレイヤーを直接操作することについても警告するので、サブレイヤーを追加すると言いますcontents(「レイヤーオブジェクトがビューオブジェクトに関連付けられている場合、このプロパティのコンテンツを直接設定することは避けてください。通常、ビューとレイヤーの間の相互作用により、後続の更新時にこのプロパティのコンテンツを置き換えるビューで。」)このアプローチは特にお勧めしません。時期尚早の描画は、パフォーマンスと描画品質を損ないます。しかし、何らかの理由でそれcontentsが必要な場合は、それを取得する方法です。
Rob Napier 2013年

@RobNapierポイントはdrawRect:直接呼び出して取得されます。この手法を示す必要はなく、修正されました。
Slipp D. Thompson 2013

@RobNapier contentsあなたが提案しているテクニックについては...魅力的に聞こえます。私は最初にそのようなものを試しましたが、それを機能させることができませんでした。上記の解決策は、パフォーマンスが同じではない理由もなく、コードがはるかに少ないことがわかりました。ただし、contentsアプローチの実用的な解決策がある場合は、それを読んでみたいと思います(この質問に2つの答えが得られない理由はありませんよね?)
Slipp D. Thompson

@RobNapier注:これはCALayerとは関係ありませんdisplay。これは、GLKViewで行われるのと同じように、UIViewサブクラスのカスタムパブリックメソッドです(別のUIViewサブクラス、および私が知っている唯一のAppleが作成したもので、DRAW NOW!機能が必要です)。
Slipp D. Thompson 2013

5

私も同じ問題を抱えていて、SOやGoogleのソリューションがすべてうまくいきませんでした。通常はsetNeedsDisplay機能しますが、機能しない場合は... 可能なすべてのスレッドやその他のものから可能な限りあらゆる方法でビューを
呼び出してみましたsetNeedsDisplayが、まだ成功していません。ロブが言ったように、私たちは知っています

「これは次の描画サイクルで描画する必要があります。」

しかし、何らかの理由で今回は描画されません。そして、私が見つけた唯一の解決策は、次のように、ドローをブロックするものをすべて通過させるために、しばらくしてから手動で呼び出すことです。

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 
                                        (int64_t)(0.005 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
    [viewToRefresh setNeedsDisplay];
});

ビューを頻繁に再描画する必要がない場合、これは良い解決策です。それ以外の場合、移動(アクション)を行う場合、通常はを呼び出すだけで問題はありませんsetNeedsDisplay

私がそうだったように、それがそこで失われた人を助けることを願っています。


0

これは大きな変更であるか、プロジェクトには適していない可能性があることはわかっていますが、すでにデータを取得するまでプッシュを実行しないことを検討しましたかか?そうすれば、ビューを一度描画するだけで済み、ユーザーエクスペリエンスも向上します-プッシュはロード済みで移動します。

これを行う方法は、UITableView didSelectRowAtIndexPath非同期でデータを要求することです。応答を受け取ったら、手動でセグエを実行し、データをのviewControllerに渡しますprepareForSegue。一方、いくつかのアクティビティインジケーターを表示したい場合があります。単純な読み込みインジケーターについては、https://github.com/jdg/MBProgressHUDを確認してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.