UICollectionView reloadData не работает должным образом в iOS 7

93

Я обновляю свои приложения для работы на iOS 7, что по большей части проходит гладко. Я заметил в более чем одном приложении, что reloadDataметод a UICollectionViewControllerработает не совсем так, как раньше.

Я загружу UICollectionViewController, заполню UICollectionViewнекоторыми данными как обычно. Это отлично работает в первый раз. Однако, если я запрашиваю новые данные (заполняю UICollectionViewDataSource), а затем вызываю reloadData, он запрашивает у источника данных numberOfItemsInSectionи numberOfSectionsInCollectionView, но, похоже, не вызывает cellForItemAtIndexPathнужное количество раз.

Если я изменю код, чтобы перезагрузить только один раздел, он будет работать правильно. Для меня нетрудно изменить их, но я не думаю, что мне следовало бы это делать. reloadDataдолжен перезагрузить все видимые ячейки согласно документации.

Кто-нибудь еще видел это?

Волк
источник
5
То же самое здесь, это в iOS7GM, раньше работало нормально. Я заметил, что вызов reloadDataпосле viewDidAppear, похоже, решает проблему, это ужасный обходной путь и требует исправления. Надеюсь, здесь кто-то поможет.
jasonIM
1
Такая же проблема. Код раньше нормально работал в iOS6. теперь не вызывает cellforitematindexpath, хотя возвращает правильное количество ячеек
Авнер Барр,
Было ли это исправлено в выпуске после 7.0?
Уильям Джокуш
У меня все еще возникают проблемы, связанные с этой проблемой.
Анил
Аналогичная проблема после изменения [collectionView setFrame] на лету; всегда удаляет одну ячейку, и это независимо от числа в источнике данных. Пробовал все здесь и даже больше, и не могу обойтись.
RegularExpression

Ответы:

72

Заставьте это в основном потоке:

dispatch_async(dispatch_get_main_queue(), ^ {
    [self.collectionView reloadData];
});
Шаунти Фондриси
источник
1
Я не уверен, что смогу объяснить больше. После поиска, исследования, тестирования и изучения. Я считаю, что это ошибка iOS 7. Принудительный основной поток запустит все сообщения, связанные с UIKit. Кажется, я сталкиваюсь с этим при переходе к представлению из другого контроллера представления. Обновляю данные на viewWillAppear. Я мог видеть вызов перезагрузки представления данных и коллекции, но пользовательский интерфейс не был обновлен. Принудительный основной поток (поток пользовательского интерфейса), и он волшебным образом начинает работать. Это только в IOS 7.
Shaunti Fondrisi 02
6
Это не имеет особого смысла, потому что вы не можете вызывать reloadData из основного потока (вы не можете обновлять представления из основного потока), поэтому, возможно, это побочный эффект, который приводит к тому, что вы хотите, из-за некоторых условий гонки.
Рафаэль Оливейра
7
Диспетчеризация в основной очереди из основной очереди просто задерживает выполнение до следующего цикла выполнения, позволяя всему, что в данный момент находится в очереди, быть выполнено первым.
Joony
2
Благодарность!! По-прежнему не понимаю, верен ли аргумент Joony, потому что основной запрос данных потребляет его время и ответ задерживается, или потому что я перезагружаю данные на willDisplayCell.
Фидель Лопес,
1
Ух ты все это время, а это все еще всплывает. Это действительно состояние гонки или связано с жизненным циклом события просмотра. вид "Воля" появился бы уже нарисован. Хорошее понимание, Джуни, спасибо. Думаете, мы наконец сможем установить для этого пункта значение «ответил»?
Шаунти Фондриси
64

В моем случае количество ячеек / разделов в источнике данных никогда не менялось, и я просто хотел перезагрузить видимый контент на экране ..

Мне удалось обойти это, позвонив:

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

тогда:

[self.collectionView reloadData];
liamnichols
источник
6
Эта строка вызвала сбой моего приложения - «*** Ошибка утверждения в - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840»
Mugubrious
@Lugubrious Вы, вероятно, выполняете другие анимации одновременно ... попробуйте поместить их в performBatchUpdates:completion:блок?
liamnichols
Это сработало для меня, но я не уверен, что понимаю, почему это необходимо. Есть идеи, в чем проблема?
Джон Эванс
@JonEvans К сожалению, я понятия не имею ... Я считаю, что это какая-то ошибка в iOS, не уверен, была ли она решена в более поздних версиях или нет, хотя с тех пор я не тестировал, а проект, в котором у меня возникла проблема, не дольше моя проблема :)
liamnichols
1
Этот баг - полная чушь собачья! Все мои ячейки - случайным образом - исчезали, когда я перезагружал свой collectionView, только если в моей коллекции был один конкретный тип ячейки. Я потерял два дня на это, потому что не мог понять, что происходит, и теперь, когда я применил ваше решение и что оно работает, я все еще не понимаю, почему оно теперь работает. Это так неприятно! В любом случае спасибо за помощь: D !!
CyberDandy
26

У меня была точно такая же проблема, однако мне удалось выяснить, что происходит не так. В моем случае я вызывал reloadData из collectionView: cellForItemAtIndexPath: что, похоже, неверно.

Отправка вызова reloadData в основную очередь решила проблему раз и навсегда.

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
  });
Антон Матосов
источник
1
Вы можете сказать мне, что это за строка для [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper
Это решило и для меня - в моем случае меня reloadDataвызвал наблюдатель изменений.
sudo make install
Также это касаетсяcollectionView(_:willDisplayCell:forItemAtIndexPath:)
Стефана Арамбашича
20

Перезагрузка некоторых предметов у меня не сработала. В моем случае, и только потому, что в collectionView, который я использую, есть только один раздел, я просто перезагружаю этот конкретный раздел. На этот раз содержимое перезагружено правильно. Странно, что это происходит только на iOS 7 (7.0.3)

[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
Мигельсанчес
источник
12

У меня была такая же проблема с reloadData на iOS 7. После долгого сеанса отладки я обнаружил проблему.

В iOS7 reloadData в UICollectionView не отменяет предыдущие обновления, которые еще не завершены (обновления, которые вызываются внутри блока performBatchUpdates:).

Лучшее решение для устранения этой ошибки - остановить все обновления, которые в настоящее время обрабатываются, и вызвать reloadData. Я не нашел способа отменить или остановить блок performBatchUpdates. Поэтому, чтобы исправить ошибку, я сохранил флаг, который указывает, есть ли блок performBatchUpdates, который в данный момент обрабатывается. Если нет блока обновления, который в настоящее время обрабатывается, я могу немедленно вызвать reloadData, и все будет работать, как ожидалось. Если есть блок обновления, который в настоящее время обрабатывается, я вызываю reloadData для всего блока performBatchUpdates.

user2459624
источник
Где вы выполняете все свои обновления внутри performBatchUpdate? Некоторые в некоторых нет? Все вон? Очень интересный пост.
VaporwareWolf
Я использую представление коллекции с NSFetchedResultsController для отображения данных из CoreData. Когда делегат NSFetchedResultsController уведомляет об изменениях, я собираю все обновления и вызываю их внутри performBatchUpdates. При изменении предиката запроса NSFetchedResultsController необходимо вызвать reloadData.
user2459624
На самом деле это хороший ответ на вопрос. Если вы запустите reloadItems () (который анимирован), а затем reloadData (), он пропустит ячейки.
биография
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

У меня тоже была такая проблема. По совпадению я добавил кнопку в верхней части коллекции, чтобы принудительно перезагрузить для тестирования - и внезапно методы начали вызываться.

Также просто добавляем что-то простое, как

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

вызовет методы

Также я поигрался с размером кадра - и вуаля вызывались методы.

В iOS7 UICollectionView много ошибок.

Авнер Барр
источник
Я рад видеть (в определенной степени), что другие тоже сталкиваются с этой проблемой. Спасибо за обходной путь.
VaporwareWolf
3

Вы можете использовать этот метод

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Вы можете добавить все свои indexPathобъекты UICollectionViewв массив arrayOfAllIndexPaths, повторяя цикл для всех разделов и строк с использованием метода ниже

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

Надеюсь, вы поняли, и это может решить вашу проблему. Если вам нужны дополнительные объяснения, ответьте.

iDevAmit
источник
3

Решение, данное Шаунти Фондриси, почти идеальное. Но такой фрагмент кода или такие коды, как постановка в очередь выполнения UICollectionView's reloadData()to NSOperationQueue', mainQueueдействительно помещает время выполнения в начало следующего цикла событий в цикле выполнения, что может сделать UICollectionViewобновление одним щелчком.

Чтобы решить эту проблему. Мы должны поместить время выполнения одного и того же фрагмента кода в конец текущего цикла событий, но не в начало следующего. И мы можем добиться этого, используя CFRunLoopObserver.

CFRunLoopObserver наблюдает за всеми действиями ожидания источника ввода, а также за действиями входа и выхода цикла выполнения.

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

Среди этих действий .AfterWaitingможно наблюдать, когда текущий цикл событий приближается к завершению, и .BeforeWaitingможно наблюдать, когда только что начался следующий цикл событий.

Поскольку существует только один NSRunLoopэкземпляр для каждого NSThreadи NSRunLoopточно управляет NSThread, мы можем считать, что доступы поступают из одного и того же NSRunLoopэкземпляра, никогда не пересекаются потоки.

Основываясь на упомянутых выше моментах, теперь мы можем написать код: диспетчер задач на основе 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
        }
    }
}

Используя предыдущий код, теперь мы можем отправить выполнение UICollectionView's reloadData()в конец текущего цикла событий с помощью такого фрагмента кода:

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

Фактически, такой диспетчер задач на основе NSRunLoop уже был в одном из моих личных фреймворков: Nest. А вот его репозиторий на GitHub: https://github.com/WeZZard/Nest

WeZZard
источник
2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

у меня это сработало.

Праджакта
источник
1

Прежде всего, спасибо за эту ветку, очень полезно. У меня была аналогичная проблема с перезагрузкой данных, но симптомом было то, что определенные ячейки больше не могли быть выбраны навсегда, тогда как другие могли. Нет вызова метода indexPathsForSelectedItems или его эквивалента. Отладка указала на перезагрузку данных. Я пробовал оба варианта выше; и в конечном итоге принял вариант ReloadItemsAtIndexPaths, поскольку другие параметры в моем случае не работали или заставляли представление коллекции мигать на миллисекунду или около того. Код ниже работает хорошо:

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

Это случилось со мной и в iOS 8.1 sdk, но я понял все правильно, когда заметил, что даже после обновления datasourceметода numberOfItemsInSection:не возвращалось новое количество элементов. Я обновил счетчик, и он заработал.

Виней Джайн
источник
как вы обновили этот счетчик, пожалуйста .. Все вышеперечисленные методы не сработали для меня в
Swift
0

Вы устанавливаете UICollectionView.contentInset? удалите левый и правый крайInset, после удаления все в порядке, ошибка все еще существует в iOS8.3.

Цзян Ци
источник
0

Убедитесь, что каждый из методов делегата UICollectionView выполняет то, что вы от него ожидаете. Например, если

collectionView:layout:sizeForItemAtIndexPath:

не возвращает допустимый размер, перезагрузка не сработает ...

Одед Регев
источник
0

попробуйте этот код.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

    if (visibleIdx.count) {
        [self.collectionView reloadItemsAtIndexPaths:visibleIdx];
    }
Liki qu
источник
0

Вот как у меня это работало в 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
источник
-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
источник