Bir UIView'i yeniden çizmeye zorlamanın en sağlam yolu nedir?


117

Bir öğe listesi içeren bir UITableView var. Bir öğe seçmek, daha sonra aşağıdakileri yapmaya devam eden bir viewController'ı iter. viewDidLoad yönteminden Alt görünümlerimin biri için gerekli olan veriler için bir URLRequest'i başlatıyorum - drawRect geçersiz kılınmış bir UIView alt sınıfı. Veriler buluttan geldiğinde, görünüm hiyerarşimi oluşturmaya başlarım. söz konusu alt sınıf verileri geçirir ve artık drawRect yöntemi, işlenmesi için gereken her şeye sahiptir.

Fakat.

Çünkü drawRect'i açıkça çağırmıyorum - Cocoa-Touch bunu hallediyor - Cocoa-Touch'a bu UIView alt sınıfının işlenmesini gerçekten, gerçekten istediğimi bildirmenin hiçbir yolu yok. Ne zaman? Şimdi iyi olur!

[MyView setNeedsDisplay] denedim. Bu tür bazen işe yarar. Çok sivilceli.

Bununla saatlerce boğuşuyorum. Lütfen bana bir UIView yeniden işlemesini zorlamak için çok sağlam, garantili bir yaklaşım sağlayabilir mi?

Verileri görünüme besleyen kod pasajı:

// 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];

Şerefe, Doug

Yanıtlar:


193

UIViewA'yı yeniden oluşturmaya zorlamanın garantili, sağlam yolu [myView setNeedsDisplay]. Bununla ilgili sorun yaşıyorsanız, muhtemelen şu sorunlardan biriyle karşılaşıyorsunuz:

  • Verileri gerçekten almadan önce çağırıyorsunuz veya bir -drawRect:şeyi aşırı önbelleğe alıyorsunuz .

  • Bu yöntemi çağırdığınız anda görünümün çekmesini bekliyorsunuz. Cocoa çizim sistemini kullanarak "şu anda şu anda çek" talep etmenin kasıtlı olarak hiçbir yolu yoktur. Bu, tüm görünüm birleştirme sistemini bozar, performansı bozar ve muhtemelen her türlü yapıyı yaratır. "Bunun bir sonraki çizim döngüsünde çizilmesi gerekiyor" demenin yalnızca yolu vardır.

İhtiyacınız olan şey "biraz mantık, biraz daha fazla mantık" ise, o zaman "biraz daha mantığı" ayrı bir yönteme koymanız ve onu -performSelector:withObject:afterDelay:0 gecikme ile çalıştırmanız gerekir . Bu, "biraz daha mantık" koyacaktır. sonraki çizim döngüsü. Bu tür bir kod örneği ve gerekli olabileceği bir durum için bu soruya bakın (yine de mümkünse, kodu karmaşıklaştırdığı için başka çözümler aramak en iyisidir).

İşlerin çekildiğini düşünmüyorsanız, bir kesme noktası koyun -drawRect:ve ne zaman çağrıldığınızı görün. Eğer arıyor -setNeedsDisplay, ancak bir -drawRect:sonraki olay döngüsünde çağrılmıyorsanız, o zaman görüş hiyerarşinizi inceleyin ve bir yerde zekanızı alt etmeye çalışmadığınızdan emin olun. Aşırı zeka, benim deneyimimdeki kötü çizimin 1 numaralı nedenidir. Sistemi istediğiniz şeyi yapması için nasıl kandıracağınızı en iyi bildiğinizi düşündüğünüzde, genellikle tam olarak istemediğiniz şeyi yapmasını sağlarsınız.


Rob, işte kontrol listem. 1) Verilere sahip miyim? Evet. Görünümü, connectionDidFinishLoading'den ana iş parçacığında çağrılan bir yöntemde - hideSpinner - oluşturuyorum: dolayısıyla: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Anında çizmeye ihtiyacım yok. Sadece ihtiyacım var Bugün. Şu anda tamamen rastgele ve benim kontrolüm dışında. [myView setNeedsDisplay] tamamen güvenilmez. ViewDidAppear'da [myView setNeedsDisplay] 'i çağıracak kadar ileri gittim :. Nuthin'. Kakao beni görmezden geliyor. Çıldırtıcı!!
dugla

1
Bu yaklaşımla setNeedsDisplay, arama ile gerçek çağrı arasında genellikle bir gecikme olduğunu buldum drawRect:. Çağrı güvenilirliği burada olsa da, buna "en sağlam" çözüm demezdim - sağlam bir çizim çözümü, teorik olarak, çekmeyi gerektiren müşteri koduna geri dönmeden hemen önce tüm çizimi yapmalıdır.
Slipp D. Thompson

1
Cevabınızı daha fazla ayrıntıyla aşağıya yorumladım. Yöntemleri çağırarak çizim sistemini düzensiz çalışmaya zorlamaya çalışmak, dokümantasyonun açıkça çağırmamayı söylüyor olması sağlam değildir. En iyi ihtimalle tanımlanmamış bir davranıştır. Büyük olasılıkla performansı ve çizim kalitesini düşürecektir. En kötü ihtimalle kilitlenebilir veya çökebilir.
Rob Napier

1
Geri dönmeden önce çizim yapmanız gerekiyorsa, bir görüntü bağlamı çizin (bu, çoğu insanın beklediği tuval gibi çalışır). Ancak birleştirme sistemini kandırmaya çalışmayın. Minimum kaynak ek yükü ile çok hızlı yüksek kaliteli grafikler sağlamak için tasarlanmıştır. Bunun bir kısmı, çalışma döngüsünün sonunda çizim adımlarını birleştirmektir.
Rob Napier

1
@RobNapier Neden bu kadar savunmacı olmaya başladığını anlamıyorum. Lütfen tekrar kontrol edin; API ihlali yok (önerinize göre temizlenmiş olmasına rağmen, birinin kendi yöntemini çağırmanın bir ihlal olmadığını savunuyorum) veya kilitlenme veya çökme şansı yok. Aynı zamanda bir "hile" de değil; normal bir şekilde CALayer'ın setNeedsDisplay / displayIfNeeded kullanır. Dahası, GLKView / OpenGL ile paralel bir şekilde Quartz ile çizim yapmak için bir süredir kullanıyorum; güvenli, kararlı ve listelediğiniz çözümden daha hızlı olduğu kanıtlandı. Bana inanmıyorsanız, deneyin. Burada kaybedecek hiçbir şeyin yok.
Slipp D. Thompson

52

SetNeedsDisplay ile drawRect'i çağırma arasında büyük bir gecikmeyle ilgili bir sorun yaşadım: (5 saniye). Ana iş parçacığından farklı bir iş parçacığında setNeedsDisplay'i çağırdım. Bu aramayı ana iş parçacığına taşıdıktan sonra gecikme ortadan kalktı.

Umarım bu biraz yardımcı olur.


Bu kesinlikle benim hatamdı. Ana iş parçacığı üzerinde çalıştığım izlenimi altındaydım, ancak NSLogged NSThread.isMainThread ana iş parçacığında katman değişikliklerini yapmadığım bir köşe durumu olduğunu fark edene kadar değildi. Beni saçımı çekmekten kurtardığın için teşekkürler!
Bay T

14

Bir görünümü eşzamanlı olarak (çağıran koda dönmeden önce) çizmeye zorlamanın para iadesi garantili, güçlendirilmiş somut-sağlam yolu CALayer, UIViewalt sınıfınızla olan etkileşimlerini yapılandırmaktır .

UIView alt sınıfınızda, displayNow()katmana " görüntüleme için kursu ayarlamasını " ve ardından "böyle yapmasını " söyleyen bir yöntem oluşturun :

hızlı

/// 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];
}

Ayrıca draw(_: CALayer, in: CGContext), özel / dahili çizim yönteminizi çağıracak bir yöntem uygulayın (her UIViewbiri a olduğu için çalışır CALayerDelegate) :

hızlı

/// 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();
}

Ve internalDraw(_: CGRect)hata korumalı özel yönteminizi oluşturun draw(_: CGRect):

hızlı

/// 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];
}

Ve şimdi myView.displayNow()çekmeye gerçekten-gerçekten ihtiyaç duyduğunuzda arayın (örneğin bir CADisplayLinkgeri arama gibi) . Bizim displayNow()yöntem söyleyecektir CALayeriçin displayIfNeeded()eşzamanlı kızımız geri arayacak olan draw(_:,in:)ve içinde çizim yapmak internalDraw(_:)geçmeden önce bağlamında içine çizilmiş ilgilisana görsel güncelleyerek.


Bu yaklaşım @ RobNapier'in yukarısına benzer, ancak buna displayIfNeeded()ek olarak arama avantajına da sahiptir setNeedsDisplay(), bu da onu senkronize yapar.

Bu mümkündür, çünkü CALayers yapmaktan daha fazla çizim işlevselliği ortaya çıkar - UIViewkatmanlar, görünümlerden daha düşük seviyededir ve mizanpaj içinde yüksek düzeyde yapılandırılabilir çizim amacıyla açıkça tasarlanmıştır ve (Cocoa'daki birçok şey gibi) esnek bir şekilde kullanılmak üzere tasarlanmıştır ( bir ana sınıf olarak veya bir delege olarak veya diğer çizim sistemlerine bir köprü olarak veya sadece kendi başlarına). CALayerDelegateProtokolün doğru kullanımı tüm bunları mümkün kılar.

Bir konfigürasyon hakkında daha fazla bilgi CALayers bulunabilir Çekirdek Animasyon Programlama Kılavuzu bölümüne Nesneleri Katmanı Yukarı Ayar .


İçin belgelerde drawRect:açıkça "Bu yöntemi asla doğrudan kendiniz çağırmamalısınız" dediğini unutmayın . Ayrıca, CALayer displayaçıkça "Bu yöntemi doğrudan çağırmayın" diyor. contentsDoğrudan katmanları eşzamanlı olarak çizmek istiyorsanız, bu kuralları ihlal etmenize gerek yoktur. İstediğiniz contentszaman katmanın üzerine çizebilirsiniz (arka plandaki dizilerde bile). Bunun için görünüme bir alt katman eklemeniz yeterlidir. Ancak bu, doğru birleştirme zamanına kadar beklemesi gereken ekrana koymaktan farklıdır.
Rob Napier

Dokümanlar ayrıca bir UIView katmanıyla doğrudan mesajlaşma konusunda uyarıda bulunduğundan bir alt katman eklemeyi söylüyorum contents("Katman nesnesi bir görünüm nesnesine bağlıysa, bu özelliğin içeriğini doğrudan ayarlamaktan kaçınmalısınız. Görünümler ve katmanlar arasındaki etkileşim genellikle sonuçlanır sonraki bir güncelleme sırasında bu mülkün içeriğini değiştiren görünümde. ") Bu yaklaşımı özellikle önermiyorum; erken çizim, performansı ve çizim kalitesini zayıflatır. Ama bir nedenden ötürü ihtiyacınız varsa, o contentszaman nasıl elde edeceğinizdir.
Rob Napier

@RobNapier Noktası drawRect:doğrudan arama ile alınır . Bu tekniği göstermek gerekli değildi ve düzeltildi.
Slipp D. Thompson

@RobNapier Önerdiğiniz tekniğe gelince contents... kulağa cazip geliyor. Başlangıçta böyle bir şey denedim, ancak çalışmasını sağlayamadım ve yukarıdaki çözümün, aynı performans göstermemesi için hiçbir neden olmaksızın çok daha az kod olduğunu buldum. Sizin için çalışan bir çözüm var mı Ancak, contentsyaklaşımın, bunu okuduktan ilgilenen olurdu (sağ, bu soruya iki cevap olamaz hiçbir neden yoktur?)
Slipp D. Thompson

@RobNapier Not: Bunun CALayer ile ilgisi yok display. Bu sadece bir UIView alt sınıfında özel bir genel yöntemdir, tıpkı GLKView'da yapılan gibi (başka bir UIView alt sınıfı ve bildiğim tek Apple tarafından yazılan, ŞİMDİ ÇİZE ! İşlevselliği gerektirir).
Slipp D. Thompson

5

Aynı sorunu yaşadım ve SO veya Google'ın tüm çözümleri benim için işe yaramadı. Genellikle setNeedsDisplayişe yarar, ancak işe yaramadığında ... Görünümü mümkün olan her ileti dizisinden ve başka şeylerden
çağırmayı denedim setNeedsDisplay- hala başarı yok. Biliyoruz, Rob'un dediği gibi

"bunun bir sonraki çizim döngüsünde çizilmesi gerekiyor."

Ama nedense bu sefer çekmedi. Ve bulduğum tek çözüm, bir süre sonra onu manuel olarak aramak, çekilişi engelleyen herhangi bir şeyin geçmesine izin vermek, şöyle:

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];
});

Görünüme gerçekten sık sık yeniden çizim yapmanız gerekmiyorsa, bu iyi bir çözümdür. Aksi takdirde, bazı hareketli (eylem) şeyler yapıyorsanız, genellikle sadece aramakla ilgili herhangi bir sorun olmaz setNeedsDisplay.

Umarım benim gibi orada kaybolan birine yardımcı olur.


0

Bunun büyük bir değişiklik olabileceğini veya projeniz için uygun olmayabileceğini biliyorum, ancak verileri elde edene kadar zorlamayı gerçekleştirmemeyi düşündünüz mü? Bu şekilde, görünümü yalnızca bir kez çizmeniz gerekir ve kullanıcı deneyimi de daha iyi olacaktır - itme, önceden yüklenmiş olarak hareket edecektir.

Bunu yapmanın yolu, UITableView didSelectRowAtIndexPatheşzamansız olarak verileri istemektir. Yanıtı aldıktan sonra, segmenti manuel olarak gerçekleştirir ve verileri içinde viewController cihazınıza iletirsiniz prepareForSegue. Bu arada, basit yükleme göstergesi kontrolü için bazı etkinlik göstergelerini göstermek isteyebilirsiniz https://github.com/jdg/MBProgressHUD

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.