UICollectionView reloadData non funziona correttamente in iOS 7


93

Ho aggiornato le mie app per funzionare su iOS 7 che sta procedendo senza intoppi per la maggior parte. Ho notato in più di un'app che il reloadDatametodo di a UICollectionViewControllernon si comporta come una volta.

Caricherò il UICollectionViewController, popolerò il UICollectionViewcon alcuni dati normalmente. Funziona alla grande la prima volta. Tuttavia, se richiedo nuovi dati (popolo il UICollectionViewDataSource) e quindi chiamo reloadData, interrogherà l'origine dati per numberOfItemsInSectione numberOfSectionsInCollectionView, ma non sembra chiamare cellForItemAtIndexPathil numero corretto di volte.

Se cambio il codice per ricaricare solo una sezione, funzionerà correttamente. Non è un problema per me cambiarli, ma non penso che dovrei farlo. reloadDatadovrebbe ricaricare tutte le celle visibili secondo la documentazione.

Qualcun altro l'ha visto?


5
Lo stesso qui, è in iOS7GM, ha funzionato bene prima. Ho notato che chiamare reloadDatadopo viewDidAppear sembra risolvere il problema, la sua orribile soluzione alternativa e deve essere risolto. Spero che qualcuno ti aiuti qui.
jasonIM

1
Avendo lo stesso problema. Il codice utilizzato per funzionare correttamente in iOS6. ora non chiama cellforitematindexpath anche se restituisce il numero corretto di celle
Avner Barr

Il problema è stato risolto in una versione successiva alla 7.0?
William Jockusch

Sto ancora affrontando problemi relativi a questo problema.
Anil

Problema simile dopo aver modificato [collectionView setFrame] al volo; rimuove sempre dalla coda una cella e il gioco è fatto indipendentemente dal numero nell'origine dati. Ho provato di tutto qui e di più, e non riesco ad aggirarlo.
RegularExpression

Risposte:


72

Forza questo sul thread principale:

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

1
Non sono sicuro di poter spiegare di più. Dopo la ricerca, la ricerca, il test e il sondaggio. Penso che questo sia un bug di iOS 7. Forzando il thread principale verranno eseguiti tutti i messaggi relativi a UIKit. Mi sembra di imbattermi in questo quando apro la vista da un altro controller di visualizzazione. Aggiorna i dati su viewWillAppear. Ho potuto vedere la chiamata di ricarica della vista della raccolta e dei dati, ma l'interfaccia utente non è stata aggiornata. Ha forzato il thread principale (thread dell'interfaccia utente) e inizia a funzionare magicamente. Questo è solo in IOS 7.
Shaunti Fondrisi

6
Non ha molto senso perché non puoi chiamare reloadData dal thread principale (non puoi aggiornare le visualizzazioni dal thread principale), quindi questo potrebbe essere un effetto collaterale che si traduce in ciò che desideri a causa di alcune condizioni di gara.
Raphael Oliveira

7
L'invio sulla coda principale dalla coda principale ritarda l'esecuzione fino al ciclo di esecuzione successivo, consentendo a tutto ciò che è attualmente in coda di essere eseguito per primo.
Joony

2
Grazie!! Ancora non capisco se l'argomento di Joony è corretto perché la richiesta di dati di base consuma il tempo e la sua risposta è ritardata o perché sto ricaricando i dati su willDisplayCell.
Fidel López

1
Wow, tutto questo tempo e questo viene ancora fuori. Questa è effettivamente una condizione di competizione o correlata al ciclo di vita dell'evento di visualizzazione. la visualizzazione "Will" sarebbe già stata disegnata. Buona intuizione Joony, grazie. Pensi di poter finalmente impostare questo elemento su "con risposta"?
Shaunti Fondrisi

64

Nel mio caso, il numero di celle / sezioni nell'origine dati non è mai cambiato e volevo solo ricaricare il contenuto visibile sullo schermo ..

Sono riuscito a aggirare questo problema chiamando:

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

poi:

[self.collectionView reloadData];

6
Quella riga ha causato l'arresto anomalo della mia app - "*** Errore di asserzione in - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840"
Lugubrious

@Lugubrious Probabilmente stai eseguendo altre animazioni contemporaneamente .. provi a metterle in un performBatchUpdates:completion:blocco?
liamnichols

Questo ha funzionato per me, ma non sono sicuro di capire perché sia ​​necessario. Qualche idea di quale sia il problema?
Jon Evans

@ JonEvans Sfortunatamente non ne ho idea .. Credo che sia una sorta di bug in iOS, non sono sicuro che sia stato risolto nelle versioni successive o meno, dato che non l'ho più testato da allora e il progetto in cui ho avuto il problema non lo è più il mio problema :)
liamnichols

1
Questo bug è solo pura stronzata! Tutte le mie celle stavano - a caso - scomparendo quando ho ricaricato la mia collectionView, solo se avevo un tipo specifico di cella nella mia raccolta. Ho perso due giorni perché non riuscivo a capire cosa stesse succedendo, e ora che ho applicato la tua soluzione e che funziona, ancora non capisco perché ora funzioni. È così frustrante! Comunque grazie per l'aiuto: D !!
CyberDandy

26

Ho avuto esattamente lo stesso problema, tuttavia sono riuscito a scoprire cosa stava succedendo. Nel mio caso stavo chiamando reloadData dalla collectionView: cellForItemAtIndexPath: che sembra non essere corretto.

L' invio della chiamata di reloadData alla coda principale ha risolto il problema una volta e per sempre.

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

1
puoi dirmi a cosa serve questa riga [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper

Questo ha risolto anche per me - nel mio caso reloadDataè stato chiamato da un osservatore del cambiamento.
sudo fa installare il

Anche questo vale percollectionView(_:willDisplayCell:forItemAtIndexPath:)
Stefan Arambasich

20

Ricaricare alcuni elementi non ha funzionato per me. Nel mio caso, e solo perché la collectionView che sto usando ha solo una sezione, ricarico semplicemente quella particolare sezione. Questa volta i contenuti vengono ricaricati correttamente. Strano che questo accada solo su iOS 7 (7.0.3)

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

12

Ho avuto lo stesso problema con reloadData su iOS 7. Dopo una lunga sessione di debug, ho trovato il problema.

Su iOS7, reloadData su UICollectionView non annulla gli aggiornamenti precedenti che non sono ancora stati completati (aggiornamenti che hanno chiamato all'interno di performBatchUpdates: block).

La soluzione migliore per risolvere questo bug è interrompere tutti gli aggiornamenti attualmente elaborati e chiamare reloadData. Non ho trovato un modo per annullare o interrompere un blocco di performBatchUpdates. Pertanto, per risolvere il bug, ho salvato un flag che indica se è presente un blocco performBatchUpdates attualmente elaborato. Se non è presente un blocco di aggiornamento attualmente elaborato, posso chiamare immediatamente reloadData e tutto funziona come previsto. Se è presente un blocco di aggiornamento attualmente elaborato, chiamerò reloadData sul blocco completo di performBatchUpdates.


Dove esegui tutti i tuoi aggiornamenti interni performBatchUpdate? Alcuni in alcuni fuori? Tutto fuori? Post molto interessante.
VaporwareWolf

Sto usando la visualizzazione raccolta con NSFetchedResultsController per mostrare i dati da CoreData. Quando il delegato NSFetchedResultsController notifica le modifiche, raccolgo tutti gli aggiornamenti e li chiamo all'interno di performBatchUpdates. Quando il predicato della richiesta NSFetchedResultsController viene modificato, è necessario chiamare reloadData.
user2459624

Questa è in realtà una buona risposta alla domanda. Se esegui un reloadItems () (che è animato) e poi reloadData () salterà le celle.
biografia

12

Swift 5 - 4 - 3

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

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

Swift 2

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

4

Ho avuto anche questo problema. Per coincidenza ho aggiunto un pulsante in cima alla collectionview per forzare il ricaricamento per i test - e all'improvviso i metodi hanno iniziato a essere chiamati.

Anche solo aggiungendo qualcosa di semplice come

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

causerebbe la chiamata dei metodi

Inoltre ho giocato con le dimensioni del frame - e voilà i metodi sono stati chiamati.

Ci sono molti bug con iOS7 UICollectionView.


Sono contento di vedere (in modi certi) che anche altri stanno riscontrando questo problema. Grazie per la soluzione.
VaporwareWolf

3

Puoi usare questo metodo

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Puoi aggiungere tutti gli indexPathoggetti del tuo UICollectionViewnell'array arrayOfAllIndexPathsiterando il ciclo per tutte le sezioni e le righe con l'uso del metodo seguente

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

Spero tu abbia capito e possa risolvere il tuo problema. Se hai bisogno di ulteriori spiegazioni, rispondi.


3

La soluzione data da Shaunti Fondrisi è pressoché perfetta. Ma un tale pezzo di codice o codici come accodare l'esecuzione di UICollectionView's reloadData()a NSOperationQueue' mainQueuemette effettivamente il tempo di esecuzione all'inizio del ciclo di eventi successivo nel ciclo di esecuzione, il che potrebbe rendereUICollectionView aggiornamento con un semplice tocco.

Per risolvere questo problema. Dobbiamo mettere i tempi di esecuzione della stessa parte di codice alla fine del ciclo di eventi corrente ma non all'inizio del successivo. E possiamo raggiungere questo obiettivo facendo uso diCFRunLoopObserver .

CFRunLoopObserver osserva tutte le attività di attesa della sorgente di input e l'attività di entrata e uscita del ciclo di esecuzione.

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 }
}

Tra queste attività, è .AfterWaitingpossibile osservare quando il ciclo di eventi corrente sta per terminare e.BeforeWaiting può essere osservato quando il ciclo di eventi successivo è appena iniziato.

Poiché esiste una sola NSRunLoopistanza per NSThreade NSRunLooppilota esattamente NSThread, possiamo considerare che gli accessi provengono dallo stessoNSRunLoop istanza e non incrociano mai i thread.

Sulla base dei punti menzionati prima, ora possiamo scrivere il codice: un task dispatcher basato su 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
        }
    }
}

Con il codice precedente, ora possiamo inviare l'esecuzione di UICollectionViews reloadData()alla fine del ciclo di eventi corrente tramite un tale pezzo di codice:

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

In effetti, un simile task dispatcher basato su NSRunLoop è già stato in uno dei miei framework utilizzati personali: Nest. Ed ecco il suo repository su GitHub: https://github.com/WeZZard/Nest


2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

ha funzionato per me.


1

Grazie prima di tutto per questo thread, molto utile. Ho avuto un problema simile con Ricarica dati tranne che il sintomo era che celle specifiche non potevano più essere selezionate in modo permanente mentre altre potevano. Nessuna chiamata al metodo indexPathsForSelectedItems o equivalente. Il debug ha indicato di ricaricare i dati. Ho provato entrambe le opzioni sopra; e ho finito per adottare l'opzione ReloadItemsAtIndexPaths poiché le altre opzioni non funzionavano nel mio caso o facevano lampeggiare la vista della raccolta per un millisecondo circa. Il codice seguente funziona bene:

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

È successo anche a me in iOS 8.1 sdk, ma ho capito bene quando ho notato che anche dopo aver aggiornato datasourceil metodo numberOfItemsInSection:non veniva restituito il nuovo conteggio degli elementi. Ho aggiornato il conteggio e l'ho fatto funzionare.


come hai aggiornato quel conteggio, per favore .. Tutti i metodi sopra non hanno funzionato per me in swift 3.
nyxee

0

Impostate UICollectionView.contentInset? rimuovere il bordo sinistro e destroInset, è tutto ok dopo averli rimossi, il bug esiste ancora in iOS8.3.


0

Verifica che ciascuno dei metodi UICollectionView Delegate esegua ciò che ti aspetti. Ad esempio, se

collectionView:layout:sizeForItemAtIndexPath:

non restituisce una dimensione valida, il ricaricamento non funzionerà ...


0

prova questo codice.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

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

0

Ecco come ha funzionato per me in 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];
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.