UICollectionView reloadData не працює належним чином у iOS 7


93

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

Я завантажу UICollectionViewController, заповнюю UICollectionViewдеякими даними як зазвичай. Це чудово працює в перший раз. Однак, якщо я запитую нові дані (заповнюю UICollectionViewDataSource), а потім телефоную reloadData, він запитує джерело даних для numberOfItemsInSectionта numberOfSectionsInCollectionView, але, схоже, не викликає cellForItemAtIndexPathналежну кількість разів.

Якщо я зміню код, щоб перезавантажити лише один розділ, він буде функціонувати належним чином. Для мене це не проблема, але я не думаю, що мені це доведеться. reloadDataповинен перезавантажити всі видимі комірки відповідно до документації.

Хтось ще бачив це?


5
Те саме тут, його в iOS7GM, працювало нормально і раніше. Я помітив, що дзвінок reloadDataпісля viewDidAppear, здається, вирішує проблему, її жахливе вирішення та потребує виправлення. Я сподіваюся, що хтось тут допоможе.
jasonIM

1
З тією ж проблемою. Код, який використовується для роботи в iOS6. тепер не дзвонить cellforitematindexpath, хоча повертає належну кількість комірок
Avner Barr

Це було виправлено у випуску після 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
Відправка в основній черзі з основної черги просто затримує виконання до наступного циклу запуску, дозволяючи всьому, що знаходиться в черзі, шанс виконати першим.
Джоні

2
Дякую!! Досі не розумію, чи аргумент Joony правильний, оскільки запит основних даних споживає час, а відповідь затримується або тому, що я перезавантажую дані за бажаннямDisplayCell.
Фідель Лопес,

1
Нічого собі весь цей час, і це все одно з’являється. Це справді умова перегонів або пов’язана із життєвим циклом події перегляду. вид "Воля" з'явиться вже був би намальований. Добре розуміння Джоні, Дякую. Думаєте, ми можемо остаточно встановити для цього елемента значення "відповіли"?
Shaunti Fondrisi

64

У моєму випадку кількість комірок / розділів у джерелі даних ніколи не змінювалася, і я просто хотів перезавантажити видимий вміст на екрані ..

Мені вдалося обійти це, зателефонувавши:

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

тоді:

[self.collectionView reloadData];

6
Цей рядок спричинив аварійне завершення роботи мого додатка - "*** Помилка твердження у - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840"
сміливий

@Lugubrious Ви, мабуть, виконуєте інші анімації одночасно .. спробуйте вкласти їх у performBatchUpdates:completion:блок?
liamnichols

Це працювало для мене, але я не впевнений, що розумію, для чого це потрібно. Будь-яка ідея, в чому проблема?
Джон Еванс,

@JonEvans На жаль, я не маю уявлення .. Я вважаю, що це якась помилка в iOS, не впевнений, вона була усунена в пізніших версіях чи ні, хоча, як я не перевіряв з тих пір, і проект, з яким у мене виникла проблема, немає довше моя проблема :)
liamnichols

1
Цей клоп - це просто чисте фігня! Усі мої клітинки - випадковим чином - зникали, коли я перезавантажував collectionView, лише якщо в моїй колекції був один певний тип комірок. Я втратив на ньому два дні, тому що не міг зрозуміти, що відбувається, і тепер, коли я застосував ваше рішення і що воно працює, я досі не розумію, чому він зараз працює. Це так засмучує! У будь-якому разі, дякую за допомогу: D !!
CyberDandy

26

У мене було саме таке питання, проте мені вдалося знайти те, що відбувається не так. У моєму випадку я викликав reloadData з колекціїView: cellForItemAtIndexPath: що, здається, не є коректним.

Відправлення виклику reloadData до основної черги вирішило проблему раз і назавжди.

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

1
Ви можете мені сказати, що це за рядок [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper

Це вирішило це і для мене - у моєму випадку reloadDataвикликав спостерігач змін.
встановити sudo

Також це стосуєтьсяcollectionView(_:willDisplayCell:forItemAtIndexPath:)
Стефана Арамбасіча

20

Перезавантаження деяких предметів у мене не вийшло. У моєму випадку, і лише тому, що collectionView, який я використовую, має лише один розділ, я просто перезавантажую цей конкретний розділ. Цього разу вміст буде правильно завантажено. Дивно, що це відбувається лише на iOS 7 (7.0.3)

[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];

12

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

У iOS7 reloadData на UICollectionView не скасовує попередні оновлення, які ще не завершені (оновлення, що викликаються всередині performBatchUpdates: block).

Найкращим рішенням для вирішення цієї помилки є зупинка всіх оновлень, які обробляються в даний час, і виклик reloadData. Я не знайшов способу скасувати або зупинити блок performBatchUpdates. Тому, щоб усунути помилку, я зберег прапорець, який вказує, чи є блок perforBatchUpdates, який наразі обробляється. Якщо в даний час немає блоку оновлення, який обробляється, я можу негайно зателефонувати до reloadData, і все працює як слід. Якщо є блок оновлення, який наразі обробляється, я викличу reloadData у повному блоці performBatchUpdates.


Де ви виконуєте всі свої оновлені внутрішні програми perforBatchUpdate? Деякі в деяких з? З усіх сил? Дуже цікавий пост.
VaporwareWolf

Я використовую подання колекції з NSFetchedResultsController для відображення даних із CoreData. Коли делегат NSFetchedResultsController сповіщає про зміни, я збираю всі оновлення і закликаю їх всередині performBatchUpdates. Коли предикат запиту NSFetchedResultsController змінюється, reloadData потрібно викликати.
користувач2459624

Це насправді хороша відповідь на запитання. Якщо запустити reloadItems () (який анімований), а потім reloadData (), він буде пропускати комірки.
біо

12

Стрімкий 5 - 4 - 3

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

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

Швидкий 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]];

Сподіваюся, ви зрозуміли, і це може вирішити вашу проблему. Якщо вам потрібні додаткові пояснення, будь ласка, дайте відповідь.


3

Рішення, надане Шонті Фондрісі, майже ідеальне. Але такий шматок коду або коди, як виконання у виконанні UICollectionView's reloadData()to NSOperationQueueenque, 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
        }
    }
}

За допомогою коду раніше ми тепер можемо відправити виконання UICollectionViews reloadData()до кінця поточного циклу подій таким фрагментом коду:

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

Насправді такий диспетчер завдань на базі NSRunLoop вже був в одному з моїх особистих використовуваних рамок: Nest. А ось його сховище на GitHub: https://github.com/WeZZard/Nest


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:не повертається новий кількість елементів. Я оновив кількість і працював.


як ви оновили цей підрахунок, будь ласка. Усі перераховані вище методи не спрацювали для мене в швидкому 3.
nyxee

0

Ви встановлюєте UICollectionView.contentInset? видаліть лівий і правий край Вставити, все нормально після того, як я їх видалю, помилка все ще існує в iOS8.3.


0

Перевірте, чи кожен з методів UICollectionView Delegate виконує те, що ви очікуєте від цього. Наприклад, якщо

collectionView:layout:sizeForItemAtIndexPath:

не повертає дійсний розмір, перезавантаження не буде працювати ...


0

спробуйте цей код.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

    if (visibleIdx.count) {
        [self.collectionView reloadItemsAtIndexPaths:visibleIdx];
    }

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
}

-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];
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.