UICollectionView reloadData no funciona correctamente en iOS 7

93

He estado actualizando mis aplicaciones para que se ejecuten en iOS 7, que en su mayor parte funciona sin problemas. He notado en más de una aplicación que elreloadData método de a UICollectionViewControllerno funciona como antes.

Cargaré UICollectionViewController, rellenaré UICollectionViewcon algunos datos como de costumbre. Esto funciona muy bien la primera vez. Sin embargo, si solicito nuevos datos (rellenar el UICollectionViewDataSource) y luego llamo reloadData, consultará la fuente de datos paranumberOfItemsInSection y numberOfSectionsInCollectionView, pero no parece llamar cellForItemAtIndexPathla cantidad adecuada de veces.

Si cambio el código para recargar solo una sección, funcionará correctamente. No es un problema para mí cambiarlos, pero no creo que deba hacerlo. reloadDatadebe recargar todas las celdas visibles de acuerdo con la documentación.

¿Alguien más ha visto esto?

VaporwareWolf
fuente
5
Lo mismo aquí, está en iOS7GM, funcionó bien antes. Me di cuenta de que llamar reloadDatadespués de viewDidAppear parece resolver el problema, su horrible solución y necesita solución. Espero que alguien me ayude aquí.
jasonIM
1
Tener el mismo problema. El código solía funcionar bien en iOS6. ahora no llama a cellforitematindexpath a pesar de que devuelve el número adecuado de celdas
Avner Barr
¿Se solucionó esto en una versión posterior a la 7.0?
William Jockusch
Todavía tengo problemas relacionados con este problema.
Anil
Problema similar después de cambiar [collectionView setFrame] sobre la marcha; siempre saca de la cola una celda y eso es todo, independientemente del número en la fuente de datos. Probé todo aquí y más, y no puedo evitarlo.
RegularExpression

Respuestas:

72

Fuerza esto en el hilo principal:

dispatch_async(dispatch_get_main_queue(), ^ {
    [self.collectionView reloadData];
});
Shaunti Fondrisi
fuente
1
No estoy seguro de poder explicar más. Después de buscar, investigar, probar y sondear. Siento que esto es un error de iOS 7. Forzar el hilo principal ejecutará todos los mensajes relacionados con UIKit. Parece que me encuentro con esto cuando aparezco en la vista desde otro controlador de vista. Actualizo los datos en viewWillAppear. Pude ver la llamada de recarga de la vista de datos y colección, pero la interfaz de usuario no se actualizó. Forzó el hilo principal (hilo de la interfaz de usuario) y mágicamente comienza a funcionar. Esto es solo en IOS 7.
Shaunti Fondrisi
6
No tiene mucho sentido porque no puede llamar a reloadData desde el hilo principal (no puede actualizar las vistas desde el hilo principal), por lo que tal vez sea un efecto secundario que dé como resultado lo que desea debido a algunas condiciones de carrera.
Raphael Oliveira
7
El envío en la cola principal desde la cola principal solo retrasa la ejecución hasta el siguiente ciclo de ejecución, lo que permite que todo lo que está actualmente en cola se ejecute primero.
Joony
2
¡¡Gracias!! Todavía no entiendo si el argumento de Joony es correcto porque la solicitud de datos centrales consume su tiempo y su respuesta se retrasa o porque estoy recargando datos a voluntadDisplayCell.
Fidel López
1
Wow todo este tiempo y esto sigue surgiendo. De hecho, se trata de una condición de carrera o relacionada con el ciclo de vida del evento de visualización. La vista "Will" aparecerá ya se habría dibujado. Buen conocimiento Joony, Gracias. ¿Crees que finalmente podemos configurar este elemento como "respondido"?
Shaunti Fondrisi
64

En mi caso, la cantidad de celdas / secciones en la fuente de datos nunca cambió y solo quería volver a cargar el contenido visible en la pantalla.

Me las arreglé para evitar esto llamando:

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

luego:

[self.collectionView reloadData];
liamnichols
fuente
6
Esa línea provocó que mi aplicación fallara - "*** Error de afirmación en - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840"
Lugubrious
@Lugubrious Probablemente esté realizando otras animaciones al mismo tiempo ... intente ponerlas en un performBatchUpdates:completion:bloque?
liamnichols
Esto funcionó para mí, pero no estoy seguro de entender por qué es necesario. ¿Alguna idea de cuál es el problema?
Jon Evans
@JonEvans Desafortunadamente, no tengo ni idea ... Creo que es algún tipo de error en iOS, no estoy seguro de si se ha resuelto en versiones posteriores o no, ya que no lo he probado desde entonces y el proyecto en el que tuve el problema no es ya es mi problema :)
liamnichols
1
¡Este error es pura mierda! Todas mis celdas estaban, al azar, desapareciendo cuando recargué mi collectionView, solo si tenía un tipo específico de celda en mi colección. Perdí dos días porque no podía entender lo que estaba pasando, y ahora que apliqué su solución y que funciona, todavía no entiendo por qué ahora está funcionando. ¡Eso es tan frustrante! De todos modos, gracias por la ayuda: D !!
CyberDandy
26

Tuve exactamente el mismo problema, sin embargo, me las arreglé para encontrar qué estaba pasando mal. En mi caso, estaba llamando a reloadData desde collectionView: cellForItemAtIndexPath: que parece no ser correcto.

El envío de la llamada de reloadData a la cola principal solucionó el problema una vez y para siempre.

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
  });
Anton Matosov
fuente
1
¿Puedes decirme para qué es esta línea [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper
Esto también lo resolvió para mí; en mi caso, reloadDatafue llamado por un observador de cambios.
sudo make install
También esto se aplica acollectionView(_:willDisplayCell:forItemAtIndexPath:)
Stefan Arambasich
20

La recarga de algunos elementos no funcionó para mí. En mi caso, y solo porque el collectionView que estoy usando tiene solo una sección, simplemente recargo esa sección en particular. Esta vez los contenidos se recargan correctamente. Es extraño que esto solo esté sucediendo en iOS 7 (7.0.3)

[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
miguelsanchez
fuente
12

Tuve el mismo problema con reloadData en iOS 7. Después de una larga sesión de depuración, encontré el problema.

En iOS7, reloadData en UICollectionView no cancela las actualizaciones anteriores que aún no se han completado (Actualizaciones que llamaron dentro de performBatchUpdates: block).

La mejor solución para resolver este error es detener todas las actualizaciones que se procesan actualmente y llamar a reloadData. No encontré una manera de cancelar o detener un bloque de performBatchUpdates. Por lo tanto, para resolver el error, guardé una bandera que indica si hay un bloque performBatchUpdates que se procesa actualmente. Si no hay un bloque de actualización que se procese actualmente, puedo llamar a reloadData inmediatamente y todo funciona como se esperaba. Si hay un bloque de actualización que se procesa actualmente, llamaré a reloadData en el bloque completo de performBatchUpdates.

usuario2459624
fuente
¿Dónde está realizando todas sus actualizaciones internas performBatchUpdate? ¿Algunos adentro algunos afuera? ¿Completamente? Post muy interesante.
VaporwareWolf
Estoy usando la vista de colección con NSFetchedResultsController para mostrar datos de CoreData. Cuando el delegado de NSFetchedResultsController notifica los cambios, recopilo todas las actualizaciones y las llamo dentro de performBatchUpdates. Cuando se cambia el predicado de solicitud NSFetchedResultsController, se debe llamar a reloadData.
user2459624
En realidad, esta es una buena respuesta a la pregunta. Si ejecuta un reloadItems () (que está animado) y luego reloadData () omitirá las celdas.
bio
12

Rápido 5 - 4 - 3

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

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

Swift 2

// Operation
NSOperationQueue.mainQueue().addOperationWithBlock(collectionView.reloadData)
dimpiax
fuente
4

Yo también tuve este problema. Por coincidencia, agregué un botón en la parte superior de la vista de colección para forzar la recarga para la prueba, y de repente se comenzaron a llamar los métodos.

También agregando algo tan simple como

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

haría que los métodos sean llamados

También jugué con el tamaño del marco, y listo, se estaban llamando a los métodos.

Hay muchos errores con iOS7 UICollectionView.

Avner Barr
fuente
Me alegra ver (en cierto modo) que otros también están experimentando este problema. Gracias por la solución.
VaporwareWolf
3

Puedes usar este método

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Puede agregar todos los indexPathobjetos de su UICollectionViewmatriz arrayOfAllIndexPathsiterando el ciclo para todas las secciones y filas con el uso del método siguiente

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

Espero que lo haya entendido y pueda resolver su problema. Si necesita más explicaciones, responda.

iDevAmit
fuente
3

La solución dada por Shaunti Fondrisi es casi perfecta. Pero tal fragmento de código o códigos como poner en cola la ejecución de UICollectionView's reloadData()to NSOperationQueue' s de mainQueuehecho pone el tiempo de ejecución al comienzo del siguiente ciclo de eventos en el ciclo de ejecución, lo que podría hacer la UICollectionViewactualización con un movimiento rápido.

Para solucionar este problema. Debemos poner el tiempo de ejecución de la misma pieza de código al final del ciclo de eventos actual pero no al comienzo del siguiente. Y podemos lograrlo haciendo uso de CFRunLoopObserver.

CFRunLoopObserver observa todas las actividades de espera de la fuente de entrada y la actividad de entrada y salida del ciclo de ejecución.

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

Entre esas actividades, .AfterWaitingse puede observar cuando el ciclo de eventos actual está a punto de terminar, y .BeforeWaitingse puede observar cuando acaba de comenzar el siguiente ciclo de eventos.

Como solo hay una NSRunLoopinstancia por NSThready NSRunLoopmaneja exactamente el NSThread, podemos considerar que los accesos provienen de la misma NSRunLoopinstancia, nunca cruzan subprocesos.

Según los puntos mencionados anteriormente, ahora podemos escribir el código: un despachador de tareas basado en 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 el código anterior, ahora podemos enviar la ejecución de UICollectionView's reloadData()hasta el final del ciclo de eventos actual mediante un fragmento de código:

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

De hecho, un despachador de tareas basado en NSRunLoop ya ha estado en uno de mis marcos de trabajo personales: Nest. Y aquí está su repositorio en GitHub: https://github.com/WeZZard/Nest

WeZZard
fuente
2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

funcionó para mí.

Prajakta
fuente
1

Gracias en primer lugar por este hilo, muy útil. Tuve un problema similar con Reload Data, excepto que el síntoma era que las celdas específicas ya no podían seleccionarse de manera permanente mientras que otras sí. No hay llamada al método indexPathsForSelectedItems o equivalente. Depuración indicada para recargar datos. Probé las dos opciones anteriores; y terminé adoptando la opción ReloadItemsAtIndexPaths ya que las otras opciones no funcionaron en mi caso o estaban haciendo que la vista de la colección parpadeara durante aproximadamente un milisegundo. El siguiente código funciona bien:

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];`
Stephane
fuente
0

También sucedió conmigo en iOS 8.1 sdk, pero lo entendí correctamente cuando noté que incluso después de actualizar datasourceel método numberOfItemsInSection:no devolvía el nuevo recuento de elementos. Actualicé el recuento y lo hice funcionar.

Vinay Jain
fuente
¿Cómo actualizaste ese recuento, por favor? Todos los métodos anteriores no me han funcionado en
Swift
0

¿Establece UICollectionView.contentInset? eliminar el edgeInset izquierdo y derecho, todo está bien después de que los elimino, el error todavía existe en iOS8.3.

Jiang Qi
fuente
0

Compruebe que cada uno de los métodos delegados de UICollectionView haga lo que espera que haga. Por ejemplo, si

collectionView:layout:sizeForItemAtIndexPath:

no devuelve un tamaño válido, la recarga no funcionará ...

Oded Regev
fuente
0

prueba este código.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

    if (visibleIdx.count) {
        [self.collectionView reloadItemsAtIndexPaths:visibleIdx];
    }
Liki qu
fuente
0

Así es como funcionó para mí en 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
}
Wissa
fuente
-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];
}
zszen
fuente