93

I've been updating my apps to run on iOS 7 which is going smoothly for the most part. I have noticed in more than one app that the reloadData method of a UICollectionViewController isn't acting quite how it used to.

I'll load the UICollectionViewController, populate the UICollectionView with some data as normal. This works great on the first time. However if I request new data (populate the UICollectionViewDataSource), and then call reloadData, it will query the data source for numberOfItemsInSection and numberOfSectionsInCollectionView, but it doesn't seem to call cellForItemAtIndexPath the proper number of times.

If I change the code to only reload one section, then it will function properly. This is no problem for me to change these, but I don't think I should have to. reloadData should reload all visible cells according to the documentation.

Has anyone else seen this?

Cesare
  • 8,326
  • 14
  • 64
  • 116
VaporwareWolf
  • 9,573
  • 8
  • 48
  • 78
  • 5
    Same here, its in iOS7GM, worked okay before. I noticed that calling `reloadData` after the viewDidAppear seems to solve the problem, its horrible workaround, and needs fix. I hope someone helps out here. – jasonIM Sep 15 '13 at 16:09
  • 1
    Having the same problem. Code used to work fine in iOS6 . now not calling cellforitematindexpath even though returning proper number of cells – Avner Barr Sep 23 '13 at 12:59
  • Was this fixed in a post-7.0 release? – William Jockusch May 10 '14 at 12:32
  • I'm still facing issues relating to this issue. – Anil May 18 '14 at 10:37
  • Similar problem after changing [collectionView setFrame] on-the-fly; always dequeues one cell and that's it regardless of the number in the data source. Tried everything here and more, and can't get around it. – RegularExpression May 26 '14 at 20:07
  • I think it's fixed in 7.1 – AXE Sep 23 '14 at 22:04
  • I have the same issue in IOS 9.2, @jasonIM 's workaround solves my problem. but in my case, viewWillAppear is useful enough. – Jerry Chen Jan 25 '16 at 07:11
  • I was also having troubles with reloadData not actually updating all of the displayed cells even though the numberOfItemsInSection has changed. This is with iOS 9.3. The workaround was to dispatch async the reload data onto the main thread so it runs on the next run loop. – Paul Buchanan Jul 15 '16 at 16:01

18 Answers18

72

Force this on the main thread:

dispatch_async(dispatch_get_main_queue(), ^ {
    [self.collectionView reloadData];
});
Cesare
  • 8,326
  • 14
  • 64
  • 116
Shaunti Fondrisi
  • 1,043
  • 8
  • 13
  • 1
    I'm not sure if I can explain more. After searching, researching, testing and probing. I feel this is an iOS 7 bug. Forcing the main thread will run all UIKit related messages. I seem to run into this when popping to the view from another view controller. I refresh the data on viewWillAppear. I could see the data and collection view reload call, but the UI was not updated. Forced the main thread (UI thread), and it magically starts working. This is only in IOS 7. – Shaunti Fondrisi Jan 02 '14 at 18:54
  • 6
    It doesn't make that much sense because you cannot call reloadData out of main thread (you can't update views out of the main thread) so this maybe is a side effect that results in what you want due to some race conditions. – Raphael Oliveira May 09 '14 at 14:57
  • 7
    Dispatching on the main queue from the main queue just delays execution until the next run loop, allowing everything that's currently queued a chance to execute first. – Joony Aug 13 '15 at 06:51
  • 2
    Thanks!! Still don't understand if Joony argument is correct because core data request consume it's time and it's answer is delayed or because i'm reloading data at willDisplayCell. – Fidel López Sep 11 '15 at 16:43
  • 1
    Wow all this time and this still comes up. This is indeed a race condition, or related to the view event lifecycle. view "Will" appear would have been drawn already. Good insight Joony, Thanks. Think we can set this item to "answered" finally? – Shaunti Fondrisi Aug 30 '16 at 15:24
65

In my case, the number of cells/sections in the datasource never changed and I just wanted to reload the visible content on the screen..

I managed to get around this by calling:

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

then:

[self.collectionView reloadData];
slugster
  • 47,434
  • 13
  • 92
  • 138
liamnichols
  • 12,109
  • 2
  • 40
  • 62
  • 6
    That line caused my app to crash - "*** Assertion failure in -[UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840" – Lugubrious Jul 23 '14 at 17:54
  • @Lugubrious You are probably performing other animations at the same time.. try putting them in a `performBatchUpdates:completion:` block? – liamnichols Jul 25 '14 at 15:05
  • This worked for me, but I'm not sure I understand why it's necessary. Any idea what the issue is? – Jon Evans Aug 11 '14 at 22:37
  • @JonEvans Unfortunately I have no idea.. I believe it is some kind of bug in iOS, not sure if it has been resolved in later versions or not though as I haven't tested since and the project I had the issue in is no longer my problem :) – liamnichols Aug 13 '14 at 12:15
  • @liamnichols Well, glad to hear that it's not a problem for you anymore at least! I will add to this that I had some crashing issues with this code until I reversed the order of the calls (so reloadData first and then reloadItemsAtIndexPaths), but since I don't know why it works at all I certainly can't explain why that helped! – Jon Evans Aug 14 '14 at 02:22
  • Works for me. Thanks! – AXE Aug 25 '14 at 09:50
  • 2
    This bug is just pure bullshit ! All my cells were - at random - disappearing when I reloaded my collectionView, only if I had one specific type of cell in my collection. I lost two days on it because I could not understand what was happening, and now that I applied your solution and that it works, I still do not understand why it is now working. That's so frustrating ! Anyway, thanks for the help :D !! – CyberDandy Dec 29 '14 at 16:16
  • Not sure why but i had to follow this answer to get visible cells to align & size correctly on rotation change. Thanks! – akdsouza May 28 '15 at 06:44
  • @Shaunti 's answer should work for most and it's less performance heavy – Dylan Moore Jun 26 '15 at 02:04
  • Not sure why but this saved my day. Thanks :) – Nitish Mar 03 '16 at 10:10
  • Thank you very much. It fix my issue. – Dody Rachmat Wicaksono Oct 09 '16 at 08:34
  • In my case, `reloadItemsAtIndexPaths` had to be called _after_ `reloadData` due to a data change. – n_b Mar 22 '17 at 19:36
26

I had exactly the same issue, however I managed to find what was going on wrong. In my case I was calling reloadData from the collectionView:cellForItemAtIndexPath: which looks not to be correct.

Dispatching call of reloadData to the main queue fixed the problem once and forever.

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
  });
Anton Matosov
  • 1,516
  • 1
  • 17
  • 18
20

Reloading some items didn't work for me. In my case, and only because the collectionView I'm using has just one section, I simply reload that particular section. This time the contents are correctly reloaded. Weird that this is only happening on iOS 7 (7.0.3)

[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
miguelsanchez
  • 201
  • 2
  • 2
12

I had the same issue with reloadData on iOS 7. After long debug session, I found the problem.

On iOS7, reloadData on UICollectionView doesn't cancel previous updates which haven't completed yet (Updates which called inside performBatchUpdates: block).

The best solution to solve this bug, is stopping all updates which currently processed and call reloadData. I didn't find a way to cancel or stop a block of performBatchUpdates. Therefore, to solve the bug, I saved a flag which indicates if there's a performBatchUpdates block which currently processed. If there isn't an update block which currently processed, I can call reloadData immediately and everything work as expected. If there's an update block which currently processed, I'll call reloadData on the complete block of performBatchUpdates.

user2459624
  • 181
  • 1
  • 8
  • Where you performing all of your updated inside performBatchUpdate? Some in some out? All out? Very interesting post. – VaporwareWolf Jan 23 '14 at 05:58
  • I'm using the collection view with NSFetchedResultsController to show data from CoreData. When the NSFetchedResultsController delegate notify on changes, I gather all the updates and call them inside performBatchUpdates. When the NSFetchedResultsController request predicate is changed, reloadData must be called. – user2459624 Aug 27 '14 at 13:45
  • This is actually a good answer to the question. If you run a reloadItems() (which is animated) and then reloadData() it will skip cells. – bio Jul 02 '20 at 23:02
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
  • 9,648
  • 2
  • 51
  • 41
4

I also had this problem. By coincidence I added a button on top of the collectionview in order to force reloading for testing - and all of a sudden the methods started getting called.

Also just adding something as simple as

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

would cause the methods to be called

Also I played around with the frame size - and voila the methods were getting called.

There are a lot of bugs with iOS7 UICollectionView.

Avner Barr
  • 13,049
  • 14
  • 82
  • 152
3

You can use this method

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

You can add all indexPath objects of your UICollectionView into array arrayOfAllIndexPaths by iterating the loop for all sections and rows with use of below method

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

I hope you understood and it can resolve your problem. If you need any more explanation please reply.

shashwat
  • 7,036
  • 7
  • 53
  • 86
iDevAmit
  • 1,312
  • 1
  • 19
  • 31
3

The solution given by Shaunti Fondrisi is nearly perfect. But such a piece of code or codes like enqueue the execution of UICollectionView's reloadData() to NSOperationQueue's mainQueue indeed puts the execution timing to the beginning of the next event loop in the run loop, which could make the UICollectionView update with a flick.

To solve this issue. We must put the execution timing of the same piece of code to the end of current event loop but not the beginning of the next. And we can achieve this by making use of CFRunLoopObserver.

CFRunLoopObserver observes all the input source waiting activities and the run loop's entry and exit activity.

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

Among those activities, .AfterWaiting can be observed when current event loop is about to end, and .BeforeWaiting can be observed when the next event loop just has began.

As there is only one NSRunLoop instance per NSThread and NSRunLoop exactly drives the NSThread, we can consider that accesses come from the same NSRunLoop instance always never cross threads.

Based on points mentioned before, we can now write the code: an NSRunLoop-based task dispatcher:

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

With the code before, we can now dispatch the execution of UICollectionView's reloadData() to the end of current event loop by such a piece of code:

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

In fact, such an NSRunLoop based task dispatcher has already been in one of my personal used framework: Nest. And here is its repository on GitHub: https://github.com/WeZZard/Nest

WeZZard
  • 3,387
  • 1
  • 19
  • 26
2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

it worked for me.

Prajakta
  • 29
  • 2
1

Thanks first of all for this thread, very helpful. I had a similar issue with Reload Data except the symptom was that specific cells could no longer be selected in a permanent way whereas others could. No call to indexPathsForSelectedItems method or equivalent. Debugging pointed out to Reload Data. I tried both options above ; and ended up adopting the ReloadItemsAtIndexPaths option as the other options didn't work in my case or were making the collection view flash for a milli-second or so. The code below works good:

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
  • 162
  • 13
1

Swift 5

For me, calling reloadItems(at:) with all visible items worked for me, instead of reloadData.

collectionView.reloadItems(at: collectionView.indexPathsForVisibleItems)

(Swift version of liamnichols's answer)

Mofawaw
  • 2,576
  • 1
  • 12
  • 37
0

It happened with me too in iOS 8.1 sdk, but I got it correct when I noticed that even after updating the datasource the method numberOfItemsInSection: was not returning the new count of items. I updated the count and got it working.

Vinay Jain
  • 1,603
  • 20
  • 27
  • how did you update that count please.. All the above methods have failed to work for me in swift 3. – nyxee Jul 21 '17 at 14:29
0

Do you set UICollectionView.contentInset? remove the left and right edgeInset, everything is ok after I remove them, the bug still exists in iOS8.3 .

Jiang Qi
  • 4,312
  • 3
  • 17
  • 19
0

Check that each one of the UICollectionView Delegate methods does what you expect it to do. For example, if

collectionView:layout:sizeForItemAtIndexPath:

doesn't return a valid size, the reload won't work...

Oded Regev
  • 3,235
  • 2
  • 34
  • 49
0

try this code.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

    if (visibleIdx.count) {
        [self.collectionView reloadItemsAtIndexPaths:visibleIdx];
    }
Alexan
  • 6,893
  • 13
  • 69
  • 89
Liki qu
  • 1
  • 1
0

Here is how it worked for 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
}
Wissa
  • 871
  • 14
  • 21
-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
  • 1,208
  • 1
  • 13
  • 18