UICollectionView reloadData nie działa poprawnie w iOS 7


93

Aktualizowałem swoje aplikacje, aby działały na iOS 7, który w większości przebiega płynnie. Zauważyłem w więcej niż jednej aplikacji, że reloadDatametoda a UICollectionViewControllernie działa tak, jak kiedyś.

Załaduję UICollectionViewController, wypełnię UICollectionViewdanymi jak zwykle. Działa to świetnie za pierwszym razem. Jeśli jednak zażądam nowych danych (wypełnię pole UICollectionViewDataSource), a następnie wywołam reloadData, zapyta źródło danych o numberOfItemsInSectioni numberOfSectionsInCollectionView, ale nie wydaje się, aby wywoływać cellForItemAtIndexPathodpowiednią liczbę razy.

Jeśli zmienię kod, aby przeładować tylko jedną sekcję, będzie działać poprawnie. Nie mam problemu z ich zmianą, ale nie sądzę, żebym musiał. reloadDatapowinien przeładować wszystkie widoczne komórki zgodnie z dokumentacją.

Czy ktoś jeszcze to widział?


5
To samo tutaj, w iOS7GM, wcześniej działało dobrze. Zauważyłem, że wywołanie reloadDatapo wyświetleniu viewDidAppear wydaje się rozwiązać problem, jego okropne obejście i wymaga naprawy. Mam nadzieję, że ktoś tu pomoże.
jasonIM,

1
Mając ten sam problem. Kod działał dobrze w iOS6. teraz nie wywołuje cellforitematindexpath, mimo że zwraca odpowiednią liczbę komórek
Avner Barr

Czy zostało to naprawione w wydaniu po 7.0?
William Jockusch

Nadal mam problemy związane z tym problemem.
Anil

Podobny problem po zmianie [collectionView setFrame] w locie; zawsze usuwa z kolejki jedną komórkę i to wszystko niezależnie od liczby w źródle danych. Próbowałem wszystkiego tutaj i nie tylko, i nie mogę tego obejść.
RegularExpression

Odpowiedzi:


72

Wymuś to w głównym wątku:

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

1
Nie jestem pewien, czy potrafię wyjaśnić więcej. Po wyszukiwaniu, badaniach, testowaniu i sondowaniu. Czuję, że to błąd w iOS 7. Wymuszenie głównego wątku spowoduje uruchomienie wszystkich komunikatów związanych z UIKit. Wydaje mi się, że napotykam to, gdy wyskakuję do widoku z innego kontrolera widoku. Odświeżam dane na viewWillAppear. Widziałem wywołanie ponownego ładowania widoku danych i kolekcji, ale interfejs użytkownika nie został zaktualizowany. Wymuszono główny wątek (wątek UI) i magicznie zaczyna działać. To jest tylko w IOS 7.
Shaunti Fondrisi

6
Nie ma to większego sensu, ponieważ nie możesz wywołać reloadData z głównego wątku (nie możesz aktualizować widoków z głównego wątku), więc może to być efekt uboczny, który skutkuje tym, czego chcesz z powodu niektórych warunków wyścigu.
Raphael Oliveira

7
Wysłanie do głównej kolejki z głównej kolejki tylko opóźnia wykonanie do następnej pętli uruchamiania, dzięki czemu wszystko, co aktualnie znajduje się w kolejce, ma szansę na wykonanie jako pierwsze.
Joony

2
Dzięki!! Nadal nie rozumiem, czy argument Joony jest poprawny, ponieważ podstawowe żądanie danych zużywa czas, a odpowiedź jest opóźniona lub ponieważ przeładowuję dane w willDisplayCell.
Fidel López

1
Wow przez cały ten czas, a to wciąż się pojawia. Jest to rzeczywiście stan wyścigu lub związany z cyklem życia wydarzenia widoku. Widok "Will" zostałby już narysowany. Dobra intuicja Joony, dzięki. Myślisz, że w końcu możemy ustawić ten element na „odpowiedział”?
Shaunti Fondrisi

64

W moim przypadku liczba komórek / sekcji w źródle danych nigdy się nie zmieniła i chciałem tylko przeładować widoczną zawartość na ekranie.

Udało mi się to obejść dzwoniąc:

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

następnie:

[self.collectionView reloadData];

6
Ten wiersz spowodował awarię mojej aplikacji - „*** Błąd potwierdzenia w - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840”
Lugubrious

@Lugubrious Prawdopodobnie wykonujesz inne animacje w tym samym czasie ... spróbuj umieścić je w performBatchUpdates:completion:bloku?
liamnichols

To zadziałało dla mnie, ale nie jestem pewien, czy rozumiem, dlaczego jest to konieczne. Masz jakiś pomysł, na czym polega problem?
Jon Evans,

@JonEvans Niestety nie mam pojęcia .. Uważam, że to jakiś błąd w iOS, nie jestem pewien, czy został rozwiązany w późniejszych wersjach, czy nie, ponieważ nie testowałem od tego czasu, a projekt, w którym miałem problem, to nie już mój problem :)
liamnichols

1
Ten błąd to po prostu bzdury! Wszystkie moje komórki - losowo - znikały, kiedy ponownie ładowałem moją kolekcję, tylko wtedy, gdy miałem jeden określony typ komórki w mojej kolekcji. Straciłem na tym dwa dni, ponieważ nie mogłem zrozumieć, co się dzieje, a teraz, gdy zastosowałem twoje rozwiązanie i że działa, nadal nie rozumiem, dlaczego teraz działa. To takie frustrujące! W każdym razie dzięki za pomoc: D !!
CyberDandy

26

Miałem dokładnie ten sam problem, ale udało mi się znaleźć przyczynę problemu. W moim przypadku wywołałem reloadData z collectionView: cellForItemAtIndexPath: co wygląda na niepoprawne.

Wysłanie wywołania reloadData do głównej kolejki rozwiązało problem raz na zawsze.

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

1
czy możesz mi powiedzieć, do czego służy ta linia [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper

To też rozwiązało problem - w moim przypadku reloadDatazostał wezwany przez obserwatora zmian.
sudo make install

Dotyczy to równieżcollectionView(_:willDisplayCell:forItemAtIndexPath:)
Stefan Arambasich

20

Ponowne ładowanie niektórych elementów nie zadziałało. W moim przypadku i tylko dlatego, że collectionView, której używam, ma tylko jedną sekcję, po prostu przeładowuję tę konkretną sekcję. Tym razem zawartość jest poprawnie ładowana ponownie. Dziwne, że dzieje się to tylko na iOS 7 (7.0.3)

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

12

Miałem ten sam problem z reloadData na iOS 7. Po długiej sesji debugowania znalazłem problem.

W systemie iOS7 reloadData w UICollectionView nie anuluje poprzednich aktualizacji, które jeszcze się nie zakończyły (aktualizacje, które wywołały wewnątrz performBatchUpdates: block).

Najlepszym rozwiązaniem tego błędu jest zatrzymanie wszystkich aktualnie przetwarzanych aktualizacji i wywołanie reloadData. Nie znalazłem sposobu na anulowanie lub zatrzymanie bloku performBatchUpdates. Dlatego, aby rozwiązać błąd, zapisałem flagę, która wskazuje, czy istnieje blok performBatchUpdates, który jest obecnie przetwarzany. Jeśli nie ma aktualnie przetwarzanego bloku aktualizacji, mogę natychmiast wywołać reloadData i wszystko działa zgodnie z oczekiwaniami. Jeśli istnieje blok aktualizacji, który jest obecnie przetwarzany, wywołam reloadData na całym bloku performBatchUpdates.


Gdzie wykonujesz wszystkie zaktualizowane wewnątrz performBatchUpdate? Niektórzy w niektórych? Wszyscy won? Bardzo ciekawy post.
VaporwareWolf

Używam widoku kolekcji z NSFetchedResultsController do wyświetlania danych z CoreData. Gdy delegat NSFetchedResultsController powiadamia o zmianach, zbieram wszystkie aktualizacje i wywołuję je w performBatchUpdates. Gdy predykat żądania NSFetchedResultsController zostanie zmieniony, należy wywołać reloadData.
user2459624

To właściwie dobra odpowiedź na pytanie. Jeśli uruchomisz reloadItems () (która jest animowana), a następnie reloadData (), pominie komórki.
biografia

12

Szybki 5-4-3

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

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

Szybki 2

// Operation
NSOperationQueue.mainQueue().addOperationWithBlock(collectionView.reloadData)

4

Ja też miałem ten problem. Przez przypadek dodałem przycisk na górze widoku kolekcji, aby wymusić ponowne załadowanie w celu przetestowania - i nagle zaczęły się wywoływać metody.

Po prostu dodanie czegoś tak prostego, jak

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

spowodowałoby wywołanie metod

Bawiłem się też rozmiarem klatki - i voila, metody były wywoływane.

Istnieje wiele błędów związanych z UICollectionView w iOS7.


Cieszę się, że widzę (w pewien sposób), że inni również doświadczają tego problemu. Dzięki za obejście.
VaporwareWolf

3

Możesz użyć tej metody

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Możesz dodać wszystkie indexPathobiekty UICollectionViewdo tablicy, arrayOfAllIndexPathswykonując iterację pętli dla wszystkich sekcji i wierszy za pomocą poniższej metody

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

Mam nadzieję, że zrozumiałeś i może rozwiązać twój problem. Jeśli potrzebujesz więcej wyjaśnień, odpowiedz.


3

Rozwiązanie podane przez Shaunti Fondrisi jest prawie doskonałe. Ale taki fragment kodu lub kody, takie jak umieszczanie w kolejce wykonywania poleceń UICollectionView's reloadData()to NSOperationQueue', w mainQueuerzeczywistości umieszcza czas wykonania na początku następnej pętli zdarzenia w pętli uruchamiania, co może spowodować, żeUICollectionView aktualizację jednym ruchem.

Aby rozwiązać ten problem. Musimy umieścić czas wykonania tego samego fragmentu kodu na końcu bieżącej pętli zdarzeń, ale nie na początku następnego. Możemy to osiągnąć, korzystając zCFRunLoopObserver .

CFRunLoopObserver obserwuje wszystkie oczekujące działania źródła wejściowego oraz działania wejścia i wyjścia pętli uruchamiania.

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

Wśród tych działań .AfterWaitingmożna zaobserwować, kiedy kończy się bieżąca pętla zdarzeń, oraz.BeforeWaiting można zaobserwować, gdy następna pętla zdarzeń właśnie się rozpoczęła.

Ponieważ istnieje tylko jedna NSRunLoopinstancja na NSThreadi NSRunLoopdokładnie napędów NSThread, możemy uznać, że dostęp pochodzi z tego samegoNSRunLoop instancji, nigdy nie przechodząc przez wątki.

W oparciu o punkty wymienione wcześniej, możemy teraz napisać kod: dyspozytor zadań oparty na 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
        }
    }
}

Z kodem wcześniej, możemy teraz wysyłką wykonanie UICollectionView„s reloadData()do końca obecnej pętli zdarzeń przez taki fragment kodu:

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

W rzeczywistości taki dyspozytor zadań oparty na NSRunLoop był już w jednym z moich osobistych używanych frameworków: Nest. A oto jego repozytorium na GitHub: https://github.com/WeZZard/Nest


2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

to działało dla mnie.


1

Przede wszystkim dziękuję za ten wątek, bardzo pomocny. Miałem podobny problem z Reload Data, z wyjątkiem tego, że objawem było to, że określonych komórek nie można było już wybierać na stałe, podczas gdy inne mogły. Brak wywołania metody indexPathsForSelectedItems lub jej odpowiednika. Debugowanie wskazało na ponowne załadowanie danych. Wypróbowałem obie powyższe opcje; i zakończyło się przyjęciem opcji ReloadItemsAtIndexPaths, ponieważ inne opcje nie działały w moim przypadku lub powodowały, że widok kolekcji migał przez milisekundę. Poniższy kod działa dobrze:

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

Zdarzyło mi się to również w sdk iOS 8.1, ale poprawiłem się, gdy zauważyłem, że nawet po aktualizacji datasourcemetody numberOfItemsInSection:nie zwracała nowej liczby elementów. Zaktualizowałem liczbę i uruchomiłem.


jak zaktualizowałeś ten licznik, proszę… Wszystkie powyższe metody zawiodły dla mnie w Swift 3.
nyxee

0

Czy ustawiasz UICollectionView.contentInset? usuń lewą i prawą krawędźInset, wszystko jest w porządku po ich usunięciu, błąd nadal istnieje w iOS8.3.


0

Sprawdź, czy każda z metod delegata UICollectionView robi to, czego oczekujesz. Na przykład, jeśli

collectionView:layout:sizeForItemAtIndexPath:

nie zwraca prawidłowego rozmiaru, przeładowanie nie zadziała ...


0

wypróbuj ten kod.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

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

0

Oto jak to zadziałało w 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];
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.