UICollectionView reloadData não funciona corretamente no iOS 7

93

Eu tenho atualizado meus aplicativos para rodar no iOS 7, que está indo bem na maior parte do tempo. Observei em mais de um aplicativo que o reloadDatamétodo de a UICollectionViewControllernão está agindo como antes.

Vou carregar o UICollectionViewController, preencher o UICollectionViewcom alguns dados normalmente. Isso funciona muito bem na primeira vez. No entanto, se eu solicitar novos dados (preencher o UICollectionViewDataSource) e, em seguida reloadData, chamar , ele consultará a fonte de dados por numberOfItemsInSectione numberOfSectionsInCollectionView, mas não parece chamar cellForItemAtIndexPatho número adequado de vezes.

Se eu alterar o código para recarregar apenas uma seção, ele funcionará corretamente. Não é problema para mim alterá-los, mas acho que não deveria ser necessário. reloadDatadeve recarregar todas as células visíveis de acordo com a documentação.

alguém mais viu isso?

VaporwareWolf
fonte
5
O mesmo aqui, é no iOS7GM, funcionou bem antes. Percebi que chamar reloadDataapós viewDidAppear parece resolver o problema, sua horrível solução alternativa e precisa de conserto. Espero que alguém ajude aqui.
jasonIM de
1
Tendo o mesmo problema. O código costumava funcionar bem no iOS6. agora não estou chamando cellforitematindexpath, embora retornando o número adequado de células
Avner Barr
Isso foi corrigido em uma versão pós-7.0?
William Jockusch
Ainda estou enfrentando problemas relacionados a esse problema.
Anil
Problema semelhante após alterar [collectionView setFrame] rapidamente; sempre remove da fila uma célula e é isso, independentemente do número na fonte de dados. Tentei de tudo aqui e muito mais, e não consigo contornar isso.
RegularExpression

Respostas:

72

Force isso no tópico principal:

dispatch_async(dispatch_get_main_queue(), ^ {
    [self.collectionView reloadData];
});
Shaunti Fondrisi
fonte
1
Não tenho certeza se posso explicar mais. Depois de pesquisar, pesquisar, testar e sondar. Acho que é um bug do iOS 7. Forçar o thread principal executará todas as mensagens relacionadas ao UIKit. Parece que me deparo com isso ao acessar a visualização de outro controlador de visualização. Eu atualizo os dados em viewWillAppear. Pude ver os dados e a chamada de recarregamento da visualização da coleção, mas a IU não foi atualizada. Forçou o thread principal (thread de interface do usuário), e ele magicamente começa a funcionar. Isso é apenas no IOS 7.
Shaunti Fondrisi
6
Não faz muito sentido porque você não pode chamar reloadData fora do thread principal (você não pode atualizar visualizações fora do thread principal), então isso talvez seja um efeito colateral que resulta no que você deseja devido a algumas condições de corrida.
Raphael Oliveira
7
Despachar na fila principal a partir da fila principal apenas atrasa a execução até o próximo loop de execução, permitindo que tudo que está atualmente na fila seja executado primeiro.
Joony
2
Obrigado!! Ainda não entendo se o argumento de Joony está correto porque a solicitação de dados principais consome seu tempo e sua resposta está atrasada ou porque estou recarregando dados em willDisplayCell.
Fidel López
1
Uau todo esse tempo e isso ainda surge. Esta é de fato uma condição de corrida ou relacionada ao ciclo de vida do evento de visualização. view "Will" apareça já teria sido desenhado. Boa visão, Joony, obrigado. Acha que podemos definir este item como "respondido" finalmente?
Shaunti Fondrisi
64

No meu caso, o número de células / seções na fonte de dados nunca mudou e eu só queria recarregar o conteúdo visível na tela.

Consegui contornar isso ligando para:

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

então:

[self.collectionView reloadData];
liamnichols
fonte
6
Essa linha fez meu aplicativo travar - "*** Falha de declaração em - [UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionView.m:3840"
Lúgubre
@Lugubrious Você provavelmente está executando outras animações ao mesmo tempo ... tentar colocá-las em um performBatchUpdates:completion:bloco?
liamnichols
Isso funcionou para mim, mas não tenho certeza se entendo por que é necessário. Alguma ideia de qual é o problema?
Jon Evans
@JonEvans Infelizmente não tenho ideia .. Acredito que seja algum tipo de bug no iOS, não tenho certeza se foi resolvido em versões posteriores ou não, já que não testei desde então e o projeto que tive o problema não é mais meu problema :)
liamnichols
1
Este bug é pura besteira! Todas as minhas células estavam - aleatoriamente - desaparecendo quando recarreguei meu collectionView, apenas se eu tivesse um tipo específico de célula em minha coleção. Perdi dois dias nisso porque não conseguia entender o que estava acontecendo, e agora que apliquei sua solução e ela funciona, ainda não entendo porque está funcionando agora. Isso é tão frustrante! De qualquer forma, obrigado pela ajuda: D !!
CyberDandy
26

Eu tive exatamente o mesmo problema, mas consegui descobrir o que estava acontecendo de errado. No meu caso, eu estava chamando reloadData de collectionView: cellForItemAtIndexPath: que parece não estar correto.

O envio da chamada de reloadData para a fila principal corrigiu o problema de uma vez por todas.

  dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView reloadData];
  });
Anton Matosov
fonte
1
u pode me dizer o que esta linha é para [self.collectionData.collectionViewLayout invalidateLayout];
iOSDeveloper
Isso também resolveu para mim - no meu caso, reloadDatafui chamado por um observador de mudanças.
sudo make install de
Isso também se aplica acollectionView(_:willDisplayCell:forItemAtIndexPath:)
Stefan Arambasich
20

Recarregar alguns itens não funcionou para mim. No meu caso, e apenas porque a collectionView que estou usando tem apenas uma seção, simplesmente recarrego essa seção específica. Desta vez, o conteúdo é recarregado corretamente. Estranho que isso só esteja acontecendo no iOS 7 (7.0.3)

[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
Miguelsanchez
fonte
12

Tive o mesmo problema com reloadData no iOS 7. Após uma longa sessão de depuração, encontrei o problema.

No iOS7, reloadData em UICollectionView não cancela atualizações anteriores que ainda não foram concluídas (atualizações que chamam dentro de performBatchUpdates: block).

A melhor solução para resolver este bug, é parar todas as atualizações que estão sendo processadas e chamar reloadData. Não encontrei uma maneira de cancelar ou interromper um bloco de performBatchUpdates. Portanto, para resolver o bug, salvei um sinalizador que indica se há um bloco performBatchUpdates que está sendo processado. Se não houver um bloco de atualização processado atualmente, posso chamar reloadData imediatamente e tudo funciona conforme o esperado. Se houver um bloco de atualização que está sendo processado atualmente, chamarei reloadData no bloco completo de performBatchUpdates.

user2459624
fonte
Onde você executa todas as atualizações dentro de performBatchUpdate? Alguns em alguns fora? Tudo para fora? Postagem muito interessante.
VaporwareWolf
Estou usando o modo de exibição de coleção com NSFetchedResultsController para mostrar dados de CoreData. Quando o delegado NSFetchedResultsController notifica as mudanças, eu reúno todas as atualizações e as chamo dentro de performBatchUpdates. Quando o predicado da solicitação NSFetchedResultsController é alterado, reloadData deve ser chamado.
user2459624
Na verdade, essa é uma boa resposta à pergunta. Se você executar um reloadItems () (que é animado) e então reloadData (), ele ignorará as células.
bio
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
fonte
4

Eu também tive esse problema. Por coincidência, adicionei um botão no topo da visualização da coleção para forçar o recarregamento para teste - e de repente os métodos começaram a ser chamados.

Além disso, basta adicionar algo tão simples como

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

faria com que os métodos fossem chamados

Também brinquei com o tamanho do quadro - e voila os métodos estavam sendo chamados.

Existem muitos bugs no iOS7 UICollectionView.

Avner Barr
fonte
Fico feliz em ver (de certa forma) que outras pessoas também estão enfrentando esse problema. Obrigado pela solução alternativa.
VaporwareWolf
3

Você pode usar este método

[collectionView reloadItemsAtIndexPaths:arayOfAllIndexPaths];

Você pode adicionar todos os seus indexPathobjetos UICollectionViewno array arrayOfAllIndexPathsiterando o loop para todas as seções e linhas com o uso do método abaixo

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

Espero que você tenha entendido e possa resolver seu problema. Se precisar de mais explicações, responda.

iDevAmit
fonte
3

A solução dada por Shaunti Fondrisi é quase perfeita. Mas um trecho de código ou códigos como enfileirar a execução de UICollectionView's reloadData()para NSOperationQueue' s de mainQueuefato coloca o tempo de execução no início do próximo loop de evento no loop de execução, o que poderia fazer a UICollectionViewatualização com um toque.

Para resolver esse problema. Devemos colocar o tempo de execução do mesmo trecho de código no final do loop de evento atual, mas não no início do próximo. E podemos conseguir isso fazendo uso de CFRunLoopObserver.

CFRunLoopObserver observa todas as atividades de espera da fonte de entrada e a atividade de entrada e saída do loop de execução.

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

Entre essas atividades, .AfterWaitingpode ser observado quando o loop de evento atual está prestes a terminar e .BeforeWaitingpode ser observado quando o próximo loop de evento acaba de começar.

Como existe apenas uma NSRunLoopinstância por NSThreade NSRunLoopexatamente conduz o NSThread, podemos considerar que os acessos vêm da mesma NSRunLoopinstância sempre nunca cruzam os threads.

Com base nos pontos mencionados antes, agora podemos escrever o código: um despachante de tarefas baseado em 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
        }
    }
}

Com o código anterior, podemos agora despachar a execução de UICollectionView's reloadData()para o final do loop de evento atual por meio de um trecho de código:

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

Na verdade, esse despachante de tarefas baseado em NSRunLoop já esteve em um dos meus frameworks pessoais usados: Nest. E aqui está seu repositório no GitHub: https://github.com/WeZZard/Nest

WeZZard
fonte
2
 dispatch_async(dispatch_get_main_queue(), ^{

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


        });

funcionou para mim.

Prajakta
fonte
1

Obrigado em primeiro lugar por este tópico, muito útil. Tive um problema semelhante com Reload Data, exceto que o sintoma era que células específicas não podiam mais ser selecionadas de forma permanente, enquanto outras podiam. Nenhuma chamada para o método indexPathsForSelectedItems ou equivalente. Depuração apontada para Recarregar Dados. Tentei as duas opções acima; e acabei adotando a opção ReloadItemsAtIndexPaths, pois as outras opções não funcionavam no meu caso ou faziam a visualização da coleção piscar por um milissegundo ou mais. O código abaixo funciona bem:

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
fonte
0

Aconteceu comigo também no SDK do iOS 8.1, mas acertei quando percebi que mesmo após atualizar datasourceo método numberOfItemsInSection:não estava retornando a nova contagem de itens. Eu atualizei a contagem e comecei a funcionar.

Vinay Jain
fonte
como você atualizou essa contagem, por favor ... Todos os métodos acima não funcionaram para mim no swift 3.
nyxee
0

Você define UICollectionView.contentInset? remova o edgeInset esquerdo e direito, tudo está ok depois de removê-los, o bug ainda existe no iOS8.3.

Jiang Qi
fonte
0

Verifique se cada um dos métodos UICollectionView Delegate faz o que você espera que faça. Por exemplo, se

collectionView:layout:sizeForItemAtIndexPath:

não retorna um tamanho válido, o recarregamento não funciona ...

Oded Regev
fonte
0

tente este código.

 NSArray * visibleIdx = [self.collectionView indexPathsForVisibleItems];

    if (visibleIdx.count) {
        [self.collectionView reloadItemsAtIndexPaths:visibleIdx];
    }
Liki qu
fonte
0

Aqui está como funcionou para mim no 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
fonte
-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
fonte