UICollectionView reloadData tidak berfungsi dengan baik di iOS 7


93

Saya telah memperbarui aplikasi saya untuk berjalan di iOS 7 yang sebagian besar berjalan lancar. Saya telah memperhatikan di lebih dari satu aplikasi bahwa reloadDatametode a UICollectionViewControllertidak berfungsi seperti dulu.

Saya akan memuat UICollectionViewController, mengisi UICollectionViewdengan beberapa data seperti biasa. Ini berfungsi dengan baik untuk pertama kalinya. Namun jika saya meminta data baru (mengisi UICollectionViewDataSource), dan kemudian memanggil reloadData, itu akan meminta sumber data untuk numberOfItemsInSectiondan numberOfSectionsInCollectionView, tetapi tampaknya tidak memanggil cellForItemAtIndexPathjumlah yang tepat.

Jika saya mengubah kode menjadi hanya memuat ulang satu bagian, maka itu akan berfungsi dengan baik. Tidak masalah bagi saya untuk mengubahnya, tetapi saya rasa saya tidak harus melakukannya. reloadDataharus memuat ulang semua sel yang terlihat sesuai dengan dokumentasi.

Apakah ada orang lain yang melihat ini?


5
Sama di sini, di iOS7GM, berfungsi dengan baik sebelumnya. Saya perhatikan bahwa memanggil reloadDatasetelah viewDidAppear tampaknya menyelesaikan masalah, solusi yang mengerikan, dan perlu diperbaiki. Saya harap seseorang membantu di sini.
jasonIM

1
Memiliki masalah yang sama. Kode dulu berfungsi dengan baik di iOS6. sekarang tidak memanggil cellforitematindexpath meskipun mengembalikan jumlah sel yang tepat
Avner Barr

Apakah ini diperbaiki dalam rilis pasca-7.0?
William Jockusch

Saya masih menghadapi masalah terkait masalah ini.
Anil

Masalah serupa setelah mengubah [collectionView setFrame] on-the-fly; selalu mengantrekan satu sel dan hanya itu terlepas dari nomor di sumber data. Mencoba semuanya di sini dan banyak lagi, dan tidak bisa menyiasatinya.
Regular Expression

Jawaban:


72

Paksakan ini di utas utama:

dispatch_async(dispatch_get_main_queue(), ^ {
    [self.collectionView reloadData];
});

1
Saya tidak yakin apakah saya bisa menjelaskan lebih lanjut. Setelah mencari, meneliti, menguji dan menyelidik. Saya merasa ini adalah bug iOS 7. Memaksa utas utama akan menjalankan semua pesan terkait UIKit. Sepertinya saya mengalami ini ketika muncul ke tampilan dari pengontrol tampilan lain. Saya menyegarkan data di viewWillAppear. Saya bisa melihat panggilan muat ulang tampilan data dan koleksi, tetapi UI tidak diperbarui. Memaksa utas utama (utas UI), dan itu secara ajaib mulai bekerja. Ini hanya di IOS 7.
Shaunti Fondrisi

6
Ini tidak masuk akal karena Anda tidak dapat memanggil reloadData dari utas utama (Anda tidak dapat memperbarui tampilan dari utas utama) jadi ini mungkin adalah efek samping yang menghasilkan apa yang Anda inginkan karena beberapa kondisi balapan.
Raphael Oliveira

7
Pengiriman pada antrian utama dari antrian utama hanya menunda eksekusi sampai putaran proses berikutnya, memungkinkan semua yang saat ini antri mendapat kesempatan untuk dieksekusi terlebih dahulu.
Joony

2
Terima kasih!! Masih tidak mengerti apakah argumen Joony benar karena permintaan data inti menghabiskan waktunya dan jawabannya tertunda atau karena saya memuat ulang data di willDisplayCell.
Fidel López

1
Wow selama ini dan ini masih muncul. Ini memang kondisi balapan, atau terkait dengan siklus hidup peristiwa tampilan. tampilan "Will" tampaknya sudah digambar. Wawasan bagus Joony, Terima kasih. Pikirkan kita bisa mengatur item ini ke "dijawab" akhirnya?
Shaunti Fondrisi

64

Dalam kasus saya, jumlah sel / bagian dalam sumber data tidak pernah berubah dan saya hanya ingin memuat ulang konten yang terlihat di layar ..

Saya berhasil menyiasati ini dengan menelepon:

[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];

kemudian:

[self.collectionView reloadData];

6
Baris yang menyebabkan aplikasi saya untuk kecelakaan - "*** kegagalan Sikap tegas di - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840"
murung

@Lugubrious Anda mungkin melakukan animasi lain pada saat yang sama .. coba taruh di performBatchUpdates:completion:blok?
liamnichols

Ini berhasil untuk saya, tetapi saya tidak yakin saya mengerti mengapa itu perlu. Tahu apa masalahnya?
Jon Evans

@JonEvans Sayangnya saya tidak tahu .. Saya yakin ini adalah semacam bug di iOS, tidak yakin apakah itu telah diselesaikan di versi yang lebih baru atau tidak karena saya belum menguji sejak itu dan proyek yang saya hadapi masalah tidak lagi masalah saya :)
liamnichols

1
Bug ini hanya omong kosong belaka! Semua sel saya - secara acak - menghilang ketika saya memuat ulang collectionView saya, hanya jika saya memiliki satu jenis sel tertentu dalam koleksi saya. Saya kehilangan dua hari karena saya tidak mengerti apa yang terjadi, dan sekarang saya menerapkan solusi Anda dan itu berhasil, saya masih tidak mengerti mengapa sekarang berhasil. Itu sangat membuat frustasi! Pokoknya, terima kasih atas bantuannya: D !!
CyberDandy

26

Saya mengalami masalah yang persis sama, namun saya berhasil menemukan apa yang salah. Dalam kasus saya, saya memanggil reloadData dari collectionView: cellForItemAtIndexPath: yang tampaknya tidak benar.

Mengirim panggilan reloadData ke antrian utama memperbaiki masalah sekali dan selamanya.

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
  });

1
dapatkah Anda memberi tahu saya untuk apa baris ini [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper

Ini menyelesaikannya untuk saya juga - dalam kasus saya reloadDatadipanggil oleh pengamat perubahan.
sudo make install

Juga ini berlaku untukcollectionView(_:willDisplayCell:forItemAtIndexPath:)
Stefan Arambasich

20

Memuat ulang beberapa item tidak berhasil untuk saya. Dalam kasus saya, dan hanya karena collectionView yang saya gunakan hanya memiliki satu bagian, saya cukup memuat ulang bagian tersebut. Kali ini isinya dimuat ulang dengan benar. Aneh bahwa ini hanya terjadi di iOS 7 (7.0.3)

[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];

12

Saya memiliki masalah yang sama dengan reloadData di iOS 7. Setelah sesi debug yang lama, saya menemukan masalahnya.

Di iOS7, reloadData di UICollectionView tidak membatalkan pembaruan sebelumnya yang belum selesai (Pembaruan yang memanggil di dalam performBatchUpdates: block).

Solusi terbaik untuk mengatasi bug ini, menghentikan semua pembaruan yang saat ini diproses dan memanggil reloadData. Saya tidak menemukan cara untuk membatalkan atau menghentikan blok performBatchUpdates. Oleh karena itu, untuk mengatasi bug tersebut, saya menyimpan sebuah flag yang menunjukkan jika ada blok performBatchUpdates yang sedang diproses. Jika tidak ada blok pembaruan yang saat ini diproses, saya dapat segera memanggil reloadData dan semuanya berfungsi seperti yang diharapkan. Jika ada blok pembaruan yang saat ini diproses, saya akan memanggil reloadData pada blok lengkap performBatchUpdates.


Di mana Anda melakukan semua pembaruan di dalam performBatchUpdate? Beberapa di beberapa keluar? Segalanya? Posting yang sangat menarik.
VaporwareWolf

Saya menggunakan tampilan koleksi dengan NSFetchedResultsController untuk menampilkan data dari CoreData. Ketika delegasi NSFetchedResultsController memberi tahu perubahan, saya mengumpulkan semua pembaruan dan memanggilnya di dalam performBatchUpdates. Jika predikat permintaan NSFetchedResultsController diubah, reloadData harus dipanggil.
pengguna2459624

Ini sebenarnya jawaban yang bagus untuk pertanyaan itu. Jika Anda menjalankan reloadItems () (yang dianimasikan) dan kemudian reloadData () itu akan melewati sel.
bio

12

Cepat 5 - 4 - 3

// GCD    
DispatchQueue.main.async(execute: collectionView.reloadData)

// Operation
OperationQueue.main.addOperation(collectionView.reloadData)

Cepat 2

// Operation
NSOperationQueue.mainQueue().addOperationWithBlock(collectionView.reloadData)

4

Saya juga punya masalah ini. Secara kebetulan saya menambahkan tombol di atas tampilan koleksi untuk memaksa pemuatan ulang untuk pengujian - dan tiba-tiba metode tersebut mulai dipanggil.

Juga hanya menambahkan sesuatu yang sederhana seperti

UIView *aView = [UIView new];
[collectionView addSubView:aView];

akan menyebabkan metode dipanggil

Juga saya bermain-main dengan ukuran bingkai - dan voila metode dipanggil.

Ada banyak bug dengan iOS7 UICollectionView.


Saya senang melihat (dengan cara tertentu) bahwa orang lain juga mengalami masalah ini. Terima kasih atas solusinya.
VaporwareWolf

3

Anda bisa menggunakan metode ini

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Anda dapat menambahkan semua indexPathobjek UICollectionViewke dalam array arrayOfAllIndexPathsdengan mengulang loop untuk semua bagian dan baris dengan menggunakan metode di bawah ini

[aray addObject:[NSIndexPath indexPathForItem:j inSection:i]];

Saya harap Anda mengerti dan ini dapat menyelesaikan masalah Anda. Jika Anda membutuhkan penjelasan lebih lanjut, silakan balas.


3

Solusi yang diberikan oleh Shaunti Fondrisi hampir sempurna. Tapi seperti sepotong kode atau kode seperti enqueue pelaksanaan UICollectionView's reloadData()untuk NSOperationQueue' s mainQueuememang menempatkan waktu eksekusi ke awal acara loop berikutnya dalam loop berjalan, yang bisa membuat UICollectionViewpembaruan dengan sebuah film.

Untuk mengatasi masalah ini. Kita harus meletakkan waktu eksekusi dari potongan kode yang sama ke akhir loop peristiwa saat ini tetapi bukan awal berikutnya. Dan kita bisa mencapai ini dengan memanfaatkan CFRunLoopObserver.

CFRunLoopObserver mengamati semua aktivitas menunggu sumber input dan aktivitas masuk dan keluar dari run loop.

public struct CFRunLoopActivity : OptionSetType {
    public init(rawValue: CFOptionFlags)

    public static var Entry: CFRunLoopActivity { get }
    public static var BeforeTimers: CFRunLoopActivity { get }
    public static var BeforeSources: CFRunLoopActivity { get }
    public static var BeforeWaiting: CFRunLoopActivity { get }
    public static var AfterWaiting: CFRunLoopActivity { get }
    public static var Exit: CFRunLoopActivity { get }
    public static var AllActivities: CFRunLoopActivity { get }
}

Di antara aktivitas tersebut, .AfterWaitingdapat diamati saat loop peristiwa saat ini akan segera berakhir, dan .BeforeWaitingdapat diamati saat loop peristiwa berikutnya baru saja dimulai.

Karena hanya ada satu NSRunLoopinstance per NSThreaddan NSRunLoopbenar - benar mendorong NSThread, kita dapat menganggap bahwa akses yang berasal dari NSRunLoopinstance yang sama selalu tidak pernah melintasi utas.

Berdasarkan poin yang disebutkan sebelumnya, sekarang kita dapat menulis kode: dispatcher tugas berbasis NSRunLoop:

import Foundation
import ObjectiveC

public struct Weak<T: AnyObject>: Hashable {
    private weak var _value: T?
    public weak var value: T? { return _value }
    public init(_ aValue: T) { _value = aValue }

    public var hashValue: Int {
        guard let value = self.value else { return 0 }
        return ObjectIdentifier(value).hashValue
    }
}

public func ==<T: AnyObject where T: Equatable>(lhs: Weak<T>, rhs: Weak<T>)
    -> Bool
{
    return lhs.value == rhs.value
}

public func ==<T: AnyObject>(lhs: Weak<T>, rhs: Weak<T>) -> Bool {
    return lhs.value === rhs.value
}

public func ===<T: AnyObject>(lhs: Weak<T>, rhs: Weak<T>) -> Bool {
    return lhs.value === rhs.value
}

private var dispatchObserverKey =
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.DispatchObserver"

private var taskQueueKey =
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.TaskQueue"

private var taskAmendQueueKey =
"com.WeZZard.Nest.NSRunLoop.TaskDispatcher.TaskAmendQueue"

private typealias DeallocFunctionPointer =
    @convention(c) (Unmanaged<NSRunLoop>, Selector) -> Void

private var original_dealloc_imp: IMP?

private let swizzled_dealloc_imp: DeallocFunctionPointer = {
    (aSelf: Unmanaged<NSRunLoop>,
    aSelector: Selector)
    -> Void in

    let unretainedSelf = aSelf.takeUnretainedValue()

    if unretainedSelf.isDispatchObserverLoaded {
        let observer = unretainedSelf.dispatchObserver
        CFRunLoopObserverInvalidate(observer)
    }

    if let original_dealloc_imp = original_dealloc_imp {
        let originalDealloc = unsafeBitCast(original_dealloc_imp,
            DeallocFunctionPointer.self)
        originalDealloc(aSelf, aSelector)
    } else {
        fatalError("The original implementation of dealloc for NSRunLoop cannot be found!")
    }
}

public enum NSRunLoopTaskInvokeTiming: Int {
    case NextLoopBegan
    case CurrentLoopEnded
    case Idle
}

extension NSRunLoop {

    public func perform(closure: ()->Void) -> Task {
        objc_sync_enter(self)
        loadDispatchObserverIfNeeded()
        let task = Task(self, closure)
        taskQueue.append(task)
        objc_sync_exit(self)
        return task
    }

    public override class func initialize() {
        super.initialize()

        struct Static {
            static var token: dispatch_once_t = 0
        }
        // make sure this isn't a subclass
        if self !== NSRunLoop.self {
            return
        }

        dispatch_once(&Static.token) {
            let selectorDealloc: Selector = "dealloc"
            original_dealloc_imp =
                class_getMethodImplementation(self, selectorDealloc)

            let swizzled_dealloc = unsafeBitCast(swizzled_dealloc_imp, IMP.self)

            class_replaceMethod(self, selectorDealloc, swizzled_dealloc, "@:")
        }
    }

    public final class Task {
        private let weakRunLoop: Weak<NSRunLoop>

        private var _invokeTiming: NSRunLoopTaskInvokeTiming
        private var invokeTiming: NSRunLoopTaskInvokeTiming {
            var theInvokeTiming: NSRunLoopTaskInvokeTiming = .NextLoopBegan
            guard let amendQueue = weakRunLoop.value?.taskAmendQueue else {
                fatalError("Accessing a dealloced run loop")
            }
            dispatch_sync(amendQueue) { () -> Void in
                theInvokeTiming = self._invokeTiming
            }
            return theInvokeTiming
        }

        private var _modes: NSRunLoopMode
        private var modes: NSRunLoopMode {
            var theModes: NSRunLoopMode = []
            guard let amendQueue = weakRunLoop.value?.taskAmendQueue else {
                fatalError("Accessing a dealloced run loop")
            }
            dispatch_sync(amendQueue) { () -> Void in
                theModes = self._modes
            }
            return theModes
        }

        private let closure: () -> Void

        private init(_ runLoop: NSRunLoop, _ aClosure: () -> Void) {
            weakRunLoop = Weak<NSRunLoop>(runLoop)
            _invokeTiming = .NextLoopBegan
            _modes = .defaultMode
            closure = aClosure
        }

        public func forModes(modes: NSRunLoopMode) -> Task {
            if let amendQueue = weakRunLoop.value?.taskAmendQueue {
                dispatch_async(amendQueue) { [weak self] () -> Void in
                    self?._modes = modes
                }
            }
            return self
        }

        public func when(invokeTiming: NSRunLoopTaskInvokeTiming) -> Task {
            if let amendQueue = weakRunLoop.value?.taskAmendQueue {
                dispatch_async(amendQueue) { [weak self] () -> Void in
                    self?._invokeTiming = invokeTiming
                }
            }
            return self
        }
    }

    private var isDispatchObserverLoaded: Bool {
        return objc_getAssociatedObject(self, &dispatchObserverKey) !== nil
    }

    private func loadDispatchObserverIfNeeded() {
        if !isDispatchObserverLoaded {
            let invokeTimings: [NSRunLoopTaskInvokeTiming] =
            [.CurrentLoopEnded, .NextLoopBegan, .Idle]

            let activities =
            CFRunLoopActivity(invokeTimings.map{ CFRunLoopActivity($0) })

            let observer = CFRunLoopObserverCreateWithHandler(
                kCFAllocatorDefault,
                activities.rawValue,
                true, 0,
                handleRunLoopActivityWithObserver)

            CFRunLoopAddObserver(getCFRunLoop(),
                observer,
                kCFRunLoopCommonModes)

            let wrappedObserver = NSAssociated<CFRunLoopObserver>(observer)

            objc_setAssociatedObject(self,
                &dispatchObserverKey,
                wrappedObserver,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    private var dispatchObserver: CFRunLoopObserver {
        loadDispatchObserverIfNeeded()
        return (objc_getAssociatedObject(self, &dispatchObserverKey)
            as! NSAssociated<CFRunLoopObserver>)
            .value
    }

    private var taskQueue: [Task] {
        get {
            if let taskQueue = objc_getAssociatedObject(self,
                &taskQueueKey)
                as? [Task]
            {
                return taskQueue
            } else {
                let initialValue = [Task]()

                objc_setAssociatedObject(self,
                    &taskQueueKey,
                    initialValue,
                    .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

                return initialValue
            }
        }
        set {
            objc_setAssociatedObject(self,
                &taskQueueKey,
                newValue,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

        }
    }

    private var taskAmendQueue: dispatch_queue_t {
        if let taskQueue = objc_getAssociatedObject(self,
            &taskAmendQueueKey)
            as? dispatch_queue_t
        {
            return taskQueue
        } else {
            let initialValue =
            dispatch_queue_create(
                "com.WeZZard.Nest.NSRunLoop.TaskDispatcher.TaskAmendQueue",
                DISPATCH_QUEUE_SERIAL)

            objc_setAssociatedObject(self,
                &taskAmendQueueKey,
                initialValue,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

            return initialValue
        }
    }

    private func handleRunLoopActivityWithObserver(observer: CFRunLoopObserver!,
        activity: CFRunLoopActivity)
        -> Void
    {
        var removedIndices = [Int]()

        let runLoopMode: NSRunLoopMode = currentRunLoopMode

        for (index, eachTask) in taskQueue.enumerate() {
            let expectedRunLoopModes = eachTask.modes
            let expectedRunLoopActivitiy =
            CFRunLoopActivity(eachTask.invokeTiming)

            let runLoopModesMatches = expectedRunLoopModes.contains(runLoopMode)
                || expectedRunLoopModes.contains(.commonModes)

            let runLoopActivityMatches =
            activity.contains(expectedRunLoopActivitiy)

            if runLoopModesMatches && runLoopActivityMatches {
                eachTask.closure()
                removedIndices.append(index)
            }
        }

        taskQueue.removeIndicesInPlace(removedIndices)
    }
}

extension CFRunLoopActivity {
    private init(_ invokeTiming: NSRunLoopTaskInvokeTiming) {
        switch invokeTiming {
        case .NextLoopBegan:        self = .AfterWaiting
        case .CurrentLoopEnded:     self = .BeforeWaiting
        case .Idle:                 self = .Exit
        }
    }
}

Dengan kode sebelumnya, sekarang kita bisa mengirim pelaksanaan UICollectionView's reloadData()ke akhir acara loop arus dengan seperti sepotong kode:

NSRunLoop.currentRunLoop().perform({ () -> Void in
     collectionView.reloadData()
    }).when(.CurrentLoopEnded)

Faktanya, operator tugas berbasis NSRunLoop seperti itu telah ada di salah satu kerangka kerja pribadi saya: Nest. Dan inilah repositori di GitHub: https://github.com/WeZZard/Nest


2
 dispatch_async(dispatch_get_main_queue(), ^{

            [collectionView reloadData];
            [collectionView layoutIfNeeded];
            [collectionView reloadData];


        });

itu berhasil untuk saya.


1

Terima kasih pertama-tama untuk utas ini, sangat membantu. Saya memiliki masalah serupa dengan Reload Data kecuali gejalanya adalah bahwa sel tertentu tidak dapat lagi dipilih secara permanen sedangkan yang lain bisa. Tidak ada panggilan ke metode indexPathsForSelectedItems atau yang setara. Debugging menunjukkan Reload Data. Saya mencoba kedua opsi di atas; dan akhirnya mengadopsi opsi ReloadItemsAtIndexPaths karena opsi lain tidak berfungsi dalam kasus saya atau membuat tampilan koleksi berkedip selama satu mili-detik atau lebih. Kode di bawah berfungsi dengan baik:

NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; 
NSIndexPath *indexPath;
for (int i = 0; i < [self.assets count]; i++) {
         indexPath = [NSIndexPath indexPathForItem:i inSection:0];
         [indexPaths addObject:indexPath];
}
[collectionView reloadItemsAtIndexPaths:indexPaths];`

0

Itu terjadi pada saya juga di iOS 8.1 sdk, tetapi saya melakukannya dengan benar ketika saya perhatikan bahwa bahkan setelah memperbarui datasourcemetode numberOfItemsInSection:tersebut tidak mengembalikan jumlah item baru. Saya memperbarui hitungan dan membuatnya berfungsi.


bagaimana Anda memperbarui jumlah itu, tolong .. Semua metode di atas gagal berfungsi untuk saya dalam waktu 3 cepat.
nyxee

0

Apakah Anda menyetel UICollectionView.contentInset? hapus edgeInset kiri dan kanan, semuanya baik-baik saja setelah saya hapus, bug masih ada di iOS8.3.


0

Periksa apakah masing-masing metode UICollectionView Delegate melakukan apa yang Anda harapkan. Misalnya, jika

collectionView:layout:sizeForItemAtIndexPath:

tidak mengembalikan ukuran yang valid, pemuatan ulang tidak akan berfungsi ...


0

coba kode ini.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

    if (visibleIdx.count) {
        [self.collectionView reloadItemsAtIndexPaths:visibleIdx];
    }

0

Inilah cara kerjanya untuk saya di Swift 4

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

let cell = campaignsCollection.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell

cell.updateCell()

    // TO UPDATE CELLVIEWS ACCORDINGLY WHEN DATA CHANGES
    DispatchQueue.main.async {
        self.campaignsCollection.reloadData()
    }

    return cell
}

-1
inservif (isInsertHead) {
   [self insertItemsAtIndexPaths:tmpPoolIndex];
   NSArray * visibleIdx = [self indexPathsForVisibleItems];
   if (visibleIdx.count) {
       [self reloadItemsAtIndexPaths:visibleIdx];
   }
}else if (isFirstSyncData) {
    [self reloadData];
}else{
   [self insertItemsAtIndexPaths:tmpPoolIndex];
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.