Apa cara paling kuat untuk memaksa UIView menggambar ulang?

117

Saya memiliki UITableView dengan daftar item. Memilih item akan mendorong viewController yang kemudian melanjutkan untuk melakukan hal berikut. dari metode viewDidLoad Saya menjalankan URLRequest untuk data yang diperlukan oleh salah satu subview saya - subclass UIView dengan drawRect diganti. Saat data datang dari cloud, saya mulai membangun hierarki tampilan saya. subclass yang dimaksud akan melewati data dan metode drawRectnya sekarang memiliki semua yang diperlukan untuk merender.

Tapi.

Karena saya tidak menyebut drawRect secara eksplisit - Cocoa-Touch yang menanganinya - saya tidak punya cara untuk memberi tahu Cocoa-Touch bahwa saya benar-benar ingin subclass UIView ini dirender. Kapan? Sekarang akan bagus!

Saya sudah mencoba [myView setNeedsDisplay]. Ini terkadang berhasil. Sangat jerawatan.

Saya telah bergumul dengan ini selama berjam-jam. Bisakah seseorang yang memberi saya pendekatan yang kokoh dan terjamin untuk memaksa render ulang UIView.

Berikut ini cuplikan kode yang memasukkan data ke tampilan:

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

Cheers, Doug

dugla
sumber

Jawaban:

193

Cara yang terjamin dan kokoh untuk memaksa UIViewrender ulang adalah [myView setNeedsDisplay]. Jika Anda mengalami masalah dengan itu, Anda mungkin mengalami salah satu masalah berikut:

  • Anda meneleponnya sebelum Anda benar-benar memiliki datanya, atau Anda melakukan -drawRect:over-caching pada sesuatu.

  • Anda mengharapkan tampilan digambar pada saat Anda memanggil metode ini. Secara sengaja tidak ada cara untuk menuntut "gambar sekarang juga saat ini juga" menggunakan sistem gambar Kakao. Itu akan mengganggu keseluruhan sistem pengomposisian tampilan, kinerja sampah, dan kemungkinan membuat semua jenis artefak. Hanya ada cara untuk mengatakan "ini perlu digambar di siklus pengundian berikutnya."

Jika yang Anda butuhkan adalah "beberapa logika, gambar, beberapa logika lagi," maka Anda perlu meletakkan "beberapa logika lagi" dalam metode terpisah dan menjalankannya menggunakan -performSelector:withObject:afterDelay:dengan penundaan 0. Itu akan menempatkan "logika lagi" setelah siklus menggambar berikutnya. Lihat pertanyaan ini untuk contoh jenis kode tersebut, dan kasus di mana mungkin diperlukan (meskipun biasanya paling baik mencari solusi lain jika memungkinkan karena ini memperumit kode).

Jika Anda merasa tidak ada yang menarik, masukkan breakpoint -drawRect:dan lihat kapan Anda dipanggil. Jika Anda menelepon -setNeedsDisplay, tetapi -drawRect:tidak dipanggil di loop acara berikutnya, gali hierarki tampilan Anda dan pastikan Anda tidak mencoba mengakali ada di suatu tempat. Kepintaran yang berlebihan adalah penyebab # 1 menggambar yang buruk menurut pengalaman saya. Ketika Anda merasa paling tahu cara mengelabui sistem agar melakukan apa yang Anda inginkan, biasanya Anda membuatnya melakukan apa yang tidak Anda inginkan.

Rob Napier
sumber
Rob, Ini daftar periksa saya. 1) Apakah saya memiliki datanya? Iya. Saya membuat tampilan dengan metode - hideSpinner - yang dipanggil pada utas utama dari connectionDidFinishLoading: dengan demikian: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Saya tidak perlu menggambar secara instan. Saya hanya membutuhkannya. Hari ini. Saat ini, itu benar-benar acak, dan di luar kendali saya. [myView setNeedsDisplay] sama sekali tidak bisa diandalkan. Saya bahkan melangkah lebih jauh dengan memanggil [myView setNeedsDisplay] di viewDidAppear :. Nuthin '. Cocoa mengabaikan saya. Menjengkelkan !!
dugla
1
Saya telah menemukan bahwa dengan pendekatan ini sering terjadi penundaan antara setNeedsDisplaypanggilan sebenarnya dan panggilan drawRect:. Sementara keandalan panggilan ada di sini, saya tidak akan menyebutnya sebagai solusi "paling kuat" — solusi gambar yang kuat harus, secara teori, melakukan semua gambar segera sebelum kembali ke kode klien yang menuntut pengundian.
Slipp D. Thompson
1
Saya mengomentari jawaban Anda di bawah ini dengan lebih detail. Mencoba memaksa sistem gambar menjadi tidak berurutan dengan memanggil metode yang secara eksplisit dikatakan oleh dokumentasi untuk tidak memanggil adalah tidak kuat. Paling banter itu adalah perilaku yang tidak terdefinisi. Kemungkinan besar itu akan menurunkan kinerja dan kualitas gambar. Paling buruk bisa buntu atau crash.
Rob Napier
1
Jika Anda perlu menggambar sebelum kembali, gambarkan ke dalam konteks gambar (yang berfungsi seperti kanvas yang diharapkan banyak orang). Tetapi jangan mencoba mengelabui sistem pengomposisian. Ini dirancang untuk memberikan grafik berkualitas tinggi dengan sangat cepat dengan overhead sumber daya minimum. Bagian dari itu adalah penggabungan langkah menggambar di akhir putaran jalan.
Rob Napier
1
@RobNapier Saya tidak mengerti mengapa Anda menjadi begitu defensif. Silakan periksa kembali; tidak ada pelanggaran API (meskipun itu telah dibersihkan berdasarkan saran Anda, saya berpendapat bahwa memanggil metode sendiri bukanlah pelanggaran), atau kemungkinan kebuntuan atau crash. Ini juga bukan "tipuan"; itu menggunakan setNeedsDisplay / displayIfNeeded CALayer dalam mode normal. Selain itu, saya telah menggunakannya cukup lama sekarang untuk menggambar dengan Quartz dengan cara yang sejajar dengan GLKView / OpenGL; terbukti aman, stabil, dan lebih cepat daripada solusi yang Anda cantumkan. Jika Anda tidak percaya, cobalah. Anda tidak akan rugi di sini.
Slipp D. Thompson
52

Saya punya masalah dengan penundaan yang besar antara memanggil setNeedsDisplay dan drawRect: (5 detik). Ternyata saya memanggil setNeedsDisplay di utas yang berbeda dari utas utama. Setelah memindahkan panggilan ini ke utas utama, penundaan itu hilang.

Semoga ini bisa membantu.

Werner Altewischer
sumber
Ini pasti bug saya. Saya mendapat kesan bahwa saya menjalankan pada utas utama, tetapi tidak sampai saya NSLogged NSThread.isMainThread saya menyadari ada kasus sudut di mana saya TIDAK membuat perubahan lapisan pada utas utama. Terima kasih telah menyelamatkan saya dari mencabut rambut saya!
Tn. T
14

Cara yang dijamin uang kembali, diperkuat-beton-kokoh untuk memaksa tampilan menggambar secara sinkron (sebelum kembali ke kode panggilan) adalah dengan mengonfigurasi CALayerinteraksi dengan UIViewsubkelas Anda .

Di subkelas UIView Anda, buat displayNow()metode yang memberi tahu layer untuk " set course for display " lalu " make it so ":

Cepat

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

Juga terapkan draw(_: CALayer, in: CGContext)metode yang akan memanggil metode gambar pribadi / internal Anda (yang berfungsi karena setiap UIViewadalah a CALayerDelegate) :

Cepat

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

Dan buat internalDraw(_: CGRect)metode kustom Anda , bersama dengan fail-safe draw(_: CGRect):

Cepat

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

Dan sekarang panggil saja myView.displayNow()kapan pun Anda benar-benar membutuhkannya untuk menggambar (seperti dari CADisplayLinkpanggilan balik) . displayNow()Metode kami akan memberi tahu CALayerkepada displayIfNeeded(), yang akan secara sinkron memanggil kembali ke kami draw(_:,in:)dan melakukan penggambaran internalDraw(_:), memperbarui visual dengan apa yang ditarik ke dalam konteks sebelum melanjutkan.


Pendekatan ini mirip dengan @ RobNapier di atas, tetapi memiliki keuntungan memanggil displayIfNeeded()selain setNeedsDisplay(), yang membuatnya sinkron.

Hal ini dimungkinkan karena CALayers mengekspos lebih banyak fungsionalitas menggambar daripada UIViewyang dilakukannya— lapisan adalah level yang lebih rendah daripada tampilan dan dirancang secara eksplisit untuk tujuan gambar yang sangat dapat dikonfigurasi dalam tata letak, dan (seperti banyak hal di Cocoa) dirancang untuk digunakan secara fleksibel ( sebagai kelas induk, atau sebagai delegator, atau sebagai jembatan ke sistem gambar lain, atau hanya sendiri). Penggunaan CALayerDelegateprotokol yang tepat memungkinkan semua ini.

Informasi lebih lanjut tentang konfigurasi CALayers dapat ditemukan di bagian Menyiapkan Objek Lapisan dari Panduan Pemrograman Animasi Inti .

Slipp D. Thompson
sumber
Perhatikan bahwa dokumentasi untuk drawRect:secara eksplisit mengatakan "Anda tidak boleh memanggil metode ini secara langsung sendiri." Juga, CALayer displaysecara eksplisit mengatakan "Jangan panggil metode ini secara langsung." Jika Anda ingin menggambar contentslangsung pada lapisan secara serempak, Anda tidak perlu melanggar aturan ini. Anda bisa menggambar ke lapisan contentskapan saja Anda mau (bahkan pada utas latar belakang). Cukup tambahkan sublayer ke tampilan untuk itu. Tapi itu berbeda dengan menampilkannya di layar, yang perlu menunggu hingga waktu pengomposisian yang tepat.
Rob Napier
Saya katakan untuk menambahkan sublapisan karena dokumen juga memperingatkan tentang mengotak-atik lapisan UIView secara langsung contents("Jika objek lapisan terikat ke objek tampilan, Anda harus menghindari menyetel konten properti ini secara langsung. Interaksi antara tampilan dan lapisan biasanya menghasilkan dalam tampilan yang menggantikan konten properti ini selama pembaruan berikutnya. ") Saya tidak secara khusus merekomendasikan pendekatan ini; gambar yang terlalu dini merusak kinerja dan kualitas gambar. Tetapi jika Anda membutuhkannya karena suatu alasan, maka contentsbagaimana mendapatkannya.
Rob Napier
@RobNapier Point diambil dengan menelpon drawRect:secara langsung. Tidak perlu mendemonstrasikan teknik ini, dan telah diperbaiki.
Slipp D. Thompson
@RobNapier Adapun contentsteknik yang Anda sarankan… terdengar memikat. Saya mencoba sesuatu seperti itu pada awalnya tetapi tidak bisa membuatnya berfungsi, dan menemukan solusi di atas menjadi kode yang jauh lebih sedikit tanpa alasan untuk tidak sama berkinerja. Namun, jika Anda memiliki solusi yang berfungsi untuk contentspendekatan tersebut, saya akan tertarik untuk membacanya (tidak ada alasan mengapa Anda tidak dapat memiliki dua jawaban untuk pertanyaan ini, bukan?)
Slipp D. Thompson
@RobNapier Catatan: Ini tidak ada hubungannya dengan CALayer display. Ini hanya metode publik khusus dalam subkelas UIView, seperti yang dilakukan di GLKView (subkelas UIView lain, dan satu-satunya metode yang ditulis Apple yang saya ketahui yang menuntut fungsionalitas DRAW NOW! ).
Slipp D. Thompson
5

Saya memiliki masalah yang sama, dan semua solusi dari SO atau Google tidak berhasil untuk saya. Biasanya, setNeedsDisplayberhasil, tetapi ketika tidak ...
Saya telah mencoba memanggil setNeedsDisplaytampilan dengan segala cara yang mungkin dari setiap kemungkinan utas dan barang - masih tidak berhasil. Kami tahu, seperti yang dikatakan Rob, itu

"ini perlu ditarik pada siklus pengundian berikutnya."

Tapi untuk beberapa alasan kali ini tidak akan menarik. Dan satu-satunya solusi yang saya temukan adalah memanggilnya secara manual setelah beberapa waktu, untuk membiarkan apa pun yang menghalangi undian berlalu, seperti ini:

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

Ini adalah solusi yang bagus jika Anda tidak benar-benar membutuhkan tampilan untuk menggambar ulang. Sebaliknya, jika Anda melakukan beberapa hal (tindakan) bergerak, biasanya tidak ada masalah hanya dengan menelepon setNeedsDisplay.

Saya berharap ini akan membantu seseorang yang tersesat di sana, seperti saya dulu.

peraih mimpi
sumber
0

Saya tahu ini mungkin perubahan besar atau bahkan tidak cocok untuk proyek Anda, tetapi apakah Anda mempertimbangkan untuk tidak melakukan push sampai Anda sudah memiliki datanya ? Dengan begitu, Anda hanya perlu menggambar tampilan sekali dan pengalaman pengguna juga akan lebih baik - dorongan akan berpindah saat sudah dimuat.

Cara Anda melakukan ini adalah dengan UITableView didSelectRowAtIndexPathmeminta data secara asinkron. Setelah Anda menerima respons, Anda secara manual melakukan segue dan meneruskan data ke viewController Anda prepareForSegue. Sementara itu, Anda mungkin ingin menampilkan beberapa indikator aktivitas, untuk indikator pemuatan sederhana, periksa https://github.com/jdg/MBProgressHUD

Miki
sumber