UICollectionView reloadData ne fonctionne pas correctement sous iOS 7

93

J'ai mis à jour mes applications pour qu'elles fonctionnent sur iOS 7, ce qui se passe bien pour la plupart. J'ai remarqué dans plus d'une application que la reloadDataméthode de a UICollectionViewControllern'agit pas comme avant.

Je vais charger le UICollectionViewController, remplir le UICollectionViewavec des données comme d'habitude. Cela fonctionne très bien la première fois. Cependant, si je demande de nouvelles données (remplissez le UICollectionViewDataSource), puis que j'appelle reloadData, il interrogera la source de données pour numberOfItemsInSectionet numberOfSectionsInCollectionView, mais il ne semble pas appeler cellForItemAtIndexPathle bon nombre de fois.

Si je change le code pour ne recharger qu'une seule section, cela fonctionnera correctement. Ce n'est pas un problème pour moi de les changer, mais je ne pense pas que je devrais avoir à le faire. reloadDatadevrait recharger toutes les cellules visibles selon la documentation.

Quelqu'un d'autre a-t-il vu cela?

VaporwareWolf
la source
5
Même chose ici, c'est dans iOS7GM, cela fonctionnait bien avant. J'ai remarqué qu'appeler reloadDataaprès viewDidAppear semble résoudre le problème, son horrible solution de contournement et doit être corrigé. J'espère que quelqu'un aide ici.
jasonIM
1
Avoir le même problème. Le code fonctionnait bien dans iOS6. maintenant pas d'appels cellforitematindexpath même si le nombre de cellules renvoyé est correct
Avner Barr
Cela a-t-il été résolu dans une version post-7.0?
William Jockusch
Je suis toujours confronté à des problèmes liés à ce problème.
Anil
Problème similaire après avoir modifié [collectionView setFrame] à la volée; supprime toujours une cellule et c'est tout, quel que soit le nombre dans la source de données. J'ai tout essayé ici et plus, et je ne peux pas le contourner.
RegularExpression

Réponses:

72

Forcez ceci sur le thread principal:

dispatch_async(dispatch_get_main_queue(), ^ {
    [self.collectionView reloadData];
});
Shaunti Fondrisi
la source
1
Je ne sais pas si je peux en expliquer davantage. Après avoir recherché, recherché, testé et sondé. Je pense que c'est un bogue iOS 7. Forcer le fil principal exécutera tous les messages liés à UIKit. Il semble que je rencontre cela lorsque je passe à la vue depuis un autre contrôleur de vue. Je rafraîchis les données sur viewWillAppear. Je pouvais voir l'appel de rechargement de la vue des données et de la collection, mais l'interface utilisateur n'était pas mise à jour. Forcer le thread principal (thread UI), et il commence à fonctionner comme par magie. Ceci est uniquement dans IOS 7.
Shaunti Fondrisi
6
Cela n'a pas beaucoup de sens car vous ne pouvez pas appeler reloadData en dehors du thread principal (vous ne pouvez pas mettre à jour les vues hors du thread principal), donc c'est peut-être un effet secondaire qui aboutit à ce que vous voulez en raison de certaines conditions de concurrence.
Raphael Oliveira
7
La répartition sur la file d'attente principale à partir de la file d'attente principale ne fait que retarder l'exécution jusqu'à la prochaine boucle d'exécution, permettant à tout ce qui est actuellement en file d'attente de s'exécuter en premier.
Joony
2
Merci!! Je ne comprends toujours pas si l'argument de Joony est correct car la demande de données de base consomme du temps et sa réponse est retardée ou parce que je recharge les données à willDisplayCell.
Fidel López du
1
Wow tout ce temps et cela revient toujours. Il s'agit en effet d'une condition de concurrence critique ou liée au cycle de vie des événements de vue. la vue "Will" apparaîtra aurait déjà été dessinée. Bonne perspicacité Joony, merci. Pensez-vous que nous pouvons enfin définir cet élément sur "répondu"?
Shaunti Fondrisi
64

Dans mon cas, le nombre de cellules / sections dans la source de données n'a jamais changé et je voulais juste recharger le contenu visible à l'écran.

J'ai réussi à contourner cela en appelant:

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

puis:

[self.collectionView reloadData];
liamnichols
la source
6
Cette ligne a causé le plantage de mon application - "*** Échec d'assertion dans - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840"
Lugubrious
@Lugubrious Vous effectuez probablement d'autres animations en même temps .. essayez de les mettre dans un performBatchUpdates:completion:bloc?
liamnichols
Cela a fonctionné pour moi, mais je ne suis pas sûr de comprendre pourquoi c'est nécessaire. Une idée de quel est le problème?
Jon Evans
@JonEvans Malheureusement, je n'ai aucune idée .. Je pense que c'est une sorte de bogue dans iOS, je ne sais pas si cela a été résolu dans les versions ultérieures ou non car je n'ai pas testé depuis et le projet dans lequel j'ai eu le problème est non plus mon problème :)
liamnichols
1
Ce bug n'est que pure connerie! Toutes mes cellules disparaissaient - au hasard - lorsque je rechargeais ma collectionView, uniquement si j'avais un type de cellule spécifique dans ma collection. J'ai perdu deux jours dessus parce que je ne comprenais pas ce qui se passait, et maintenant que j'ai appliqué votre solution et qu'elle fonctionne, je ne comprends toujours pas pourquoi elle fonctionne maintenant. C'est tellement frustrant! Quoi qu'il en soit, merci pour l'aide: D !!
CyberDandy
26

J'ai eu exactement le même problème, mais j'ai réussi à trouver ce qui n'allait pas. Dans mon cas, j'appelais reloadData à partir de la collectionView: cellForItemAtIndexPath: ce qui ne semble pas être correct.

La distribution de l'appel de reloadData à la file d'attente principale a résolu le problème une fois pour toutes.

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
  });
Anton Matosov
la source
1
pouvez-vous me dire à quoi sert cette ligne [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper
Cela l'a résolu pour moi aussi - dans mon cas, reloadDatac'était un observateur du changement.
sudo make install le
Cela s'applique également àcollectionView(_:willDisplayCell:forItemAtIndexPath:)
Stefan Arambasich
20

Le rechargement de certains éléments n'a pas fonctionné pour moi. Dans mon cas, et uniquement parce que la collectionView que j'utilise n'a qu'une seule section, je recharge simplement cette section particulière. Cette fois, le contenu est correctement rechargé. Bizarre que cela ne se produise que sur iOS 7 (7.0.3)

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

J'ai eu le même problème avec reloadData sur iOS 7. Après une longue session de débogage, j'ai trouvé le problème.

Sur iOS7, reloadData sur UICollectionView n'annule pas les mises à jour précédentes qui ne sont pas encore terminées (mises à jour qui ont appelé à l'intérieur de performBatchUpdates: block).

La meilleure solution pour résoudre ce bogue est d'arrêter toutes les mises à jour actuellement traitées et d'appeler reloadData. Je n'ai pas trouvé de moyen d'annuler ou d'arrêter un bloc de performBatchUpdates. Par conséquent, pour résoudre le bogue, j'ai enregistré un indicateur qui indique s'il existe un bloc performBatchUpdates actuellement traité. S'il n'y a pas de bloc de mise à jour actuellement traité, je peux appeler reloadData immédiatement et tout fonctionne comme prévu. S'il y a un bloc de mise à jour qui est actuellement traité, j'appellerai reloadData sur le bloc complet de performBatchUpdates.

user2459624
la source
Où effectuez-vous toutes vos mises à jour à l'intérieur de performBatchUpdate? Certains dans certains? Tous dehors? Message très intéressant.
VaporwareWolf
J'utilise la vue de collection avec NSFetchedResultsController pour afficher les données de CoreData. Lorsque le délégué NSFetchedResultsController notifie les modifications, je rassemble toutes les mises à jour et les appelle dans performBatchUpdates. Lorsque le prédicat de demande NSFetchedResultsController est modifié, reloadData doit être appelé.
user2459624
C'est en fait une bonne réponse à la question. Si vous exécutez un reloadItems () (qui est animé) puis reloadData (), il ignorera les cellules.
bio le
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)
dimpiax
la source
4

J'ai aussi eu ce problème. Par coïncidence, j'ai ajouté un bouton au-dessus de la vue de la collection afin de forcer le rechargement pour les tests - et tout d'un coup, les méthodes ont commencé à être appelées.

Aussi simplement ajouter quelque chose d'aussi simple que

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

provoquerait l'appel des méthodes

J'ai aussi joué avec la taille du cadre - et voilà, les méthodes étaient appelées.

Il y a beaucoup de bogues avec iOS7 UICollectionView.

Avner Barr
la source
Je suis heureux de voir (de manière certienne) que d'autres connaissent également ce problème. Merci pour la solution de contournement.
VaporwareWolf
3

Vous pouvez utiliser cette méthode

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Vous pouvez ajouter tous les indexPathobjets de votre UICollectionViewtableau dans arrayOfAllIndexPathsen itérant la boucle pour toutes les sections et lignes à l'aide de la méthode ci-dessous

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

J'espère que vous avez compris et que cela pourra résoudre votre problème. Si vous avez besoin de plus d'explications, veuillez répondre.

iDevAmit
la source
3

La solution donnée par Shaunti Fondrisi est presque parfaite. Mais un tel morceau de code ou des codes comme mettre en file d'attente l'exécution de UICollectionView's reloadData()à NSOperationQueue' mainQueuemet en effet la synchronisation d'exécution au début de la prochaine boucle d'événement dans la boucle d'exécution, ce qui pourrait rendre leUICollectionView mise à jour d'un simple coup.

Pour résoudre ce problème. Nous devons mettre le timing d'exécution du même morceau de code à la fin de la boucle d'événement en cours mais pas au début de la suivante. Et nous pouvons y parvenir en utilisant CFRunLoopObserver.

CFRunLoopObserver observe toutes les activités d'attente de la source d'entrée et l'activité d'entrée et de sortie de la boucle d'exécution.

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

Parmi ces activités, .AfterWaitingpeut être observée lorsque la boucle d'événements en cours est sur le point de se terminer, et.BeforeWaiting peut être observée lorsque la prochaine boucle d'événements vient de commencer.

Comme il n'y a qu'une seule NSRunLoopinstance par NSThreadet NSRunLooppilote exactement le NSThread, on peut considérer que les accès proviennent du mêmeNSRunLoop instance ne traversent toujours jamais les threads.

Sur la base des points mentionnés précédemment, nous pouvons maintenant écrire le code: un répartiteur de tâches basé sur 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
        }
    }
}

Avec le code précédent, nous pouvons maintenant envoyer l'exécution de UICollectionView's reloadData()à la fin de la boucle d'événement actuelle par un tel morceau de code:

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

En fait, un tel répartiteur de tâches basé sur NSRunLoop a déjà été dans l'un de mes frameworks personnels: Nest. Et voici son référentiel sur GitHub: https://github.com/WeZZard/Nest

WeZZard
la source
2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

cela a fonctionné pour moi.

Prajakta
la source
1

Merci tout d'abord pour ce fil, très utile. J'ai eu un problème similaire avec Reload Data, sauf que le symptôme était que des cellules spécifiques ne pouvaient plus être sélectionnées de manière permanente alors que d'autres le pouvaient. Aucun appel à la méthode indexPathsForSelectedItems ou équivalent. Le débogage a signalé à recharger les données. J'ai essayé les deux options ci-dessus; et a fini par adopter l'option ReloadItemsAtIndexPaths car les autres options ne fonctionnaient pas dans mon cas ou faisaient clignoter la vue de la collection pendant environ une milli-seconde. Le code ci-dessous fonctionne 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];`
Stéphane
la source
0

Cela m'est également arrivé dans iOS 8.1 sdk, mais je l'ai bien compris quand j'ai remarqué que même après la mise à jour, datasourcela méthode numberOfItemsInSection:ne renvoyait pas le nouveau nombre d'éléments. J'ai mis à jour le décompte et je l'ai fait fonctionner.

Vinay Jain
la source
comment avez-vous mis à jour ce nombre s'il vous plaît .. Toutes les méthodes ci-dessus n'ont pas fonctionné pour moi dans swift 3.
nyxee
0

Définissez-vous UICollectionView.contentInset? supprimer le bord gauche et droitInset, tout va bien après les avoir supprimés, le bogue existe toujours dans iOS8.3.

Jiang Qi
la source
0

Vérifiez que chacune des méthodes UICollectionView Delegate fait ce que vous attendez d'elle. Par exemple, si

collectionView:layout:sizeForItemAtIndexPath:

ne renvoie pas une taille valide, le rechargement ne fonctionnera pas ...

Oded Regev
la source
0

essayez ce code.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

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

Voici comment cela a fonctionné pour moi dans 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
la source
-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
la source