UICollectionView reloadData 在 iOS7中不能正常工作

我一直在更新我的应用程序,以便在 iOS7上运行,大多数情况下运行都很顺利。我已经注意到在不止一个应用程序中,UICollectionViewControllerreloadData方法不再像以前那样运行。

我将加载 UICollectionViewController,像往常一样用一些数据填充 UICollectionView。这招第一次就很管用。然而,如果我请求新的数据(填充 UICollectionViewDataSource) ,然后调用 reloadData,它将查询 numberOfItemsInSectionnumberOfSectionsInCollectionView的数据源,但它似乎没有调用 cellForItemAtIndexPath的适当次数。

如果我更改代码只重新加载一个部分,那么它将正常工作。这对我来说不是问题,改变这些,但我不认为我应该有。reloadData应该根据文档重新加载所有可见单元格。

有人看过这个吗?

75774 次浏览

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];

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.

Force this on the main thread:

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

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.

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.

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];
});

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]];

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];`

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.

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 .

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

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)

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

try this code.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];


if (visibleIdx.count) {
[self.collectionView reloadItemsAtIndexPaths:visibleIdx];
}
inservif (isInsertHead) {
[self insertItemsAtIndexPaths:tmpPoolIndex];
NSArray * visibleIdx = [self indexPathsForVisibleItems];
if (visibleIdx.count) {
[self reloadItemsAtIndexPaths:visibleIdx];
}
}else if (isFirstSyncData) {
[self reloadData];
}else{
[self insertItemsAtIndexPaths:tmpPoolIndex];
}
 dispatch_async(dispatch_get_main_queue(), ^{


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




});

it worked for me.

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
}

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)