6

How to redraw non-visible UICollectionViewCell's ready for when reuse occurs???

One approach I thought of was per the code in the Layout Cell prepareForReuse function, however whilst it works it non-optimal as it causes more re-drawing then required.

Background: Need to trigger drawRect for cells after an orientation change that are not current visible, but pop up to be used and haven't been redraw, so so far I can only see that prepareForReuse would be appropriate. Issue is I'm re-drawing all "reuse" cells, whereas I really only want to redraw those that initially pop up that were created during the previous orientation position of the device.

ADDITIONAL INFO: So currently I'm doing this:

In ViewController:

override func viewWillLayoutSubviews() {
    // Clear cached layout attributes (to ensure new positions are calculated)
    (self.cal.collectionViewLayout as! GCCalendarLayout).resetCache()
    self.cal.collectionViewLayout.invalidateLayout()

    // Trigger cells to redraw themselves (to get new widths etc)
    for cell in self.cal?.visibleCells() as! [GCCalendarCell] {
        cell.setNeedsDisplay()
    }

    // Not sure how to "setNeedsDisplay" on non visible cells here?
}

In Layout Cell class:

override func prepareForReuse() {
    super.prepareForReuse()
    // Ensure "drawRect" is called (only way I could see to handle change in orientation
    self.setNeedsDisplay() 
    // ISSUE: It does this also for subsequent "prepareForReuse" after all
    // non-visible cells have been re-used and re-drawn, so really
    // not optimal
}

Example of what happens without the code in prepareForReuse above. Snapshot taken after an orientation change, and just after scrolling up a little bit:

enter image description here

Greg
  • 34,042
  • 79
  • 253
  • 454
  • cells shouldn't need to know the orientation. if you need to re-draw them when orientation changes, put that logic in the collection view instead, then let the collection view tell the cells wether they need to re-draw or not. – Simon Oct 21 '15 at 12:32
  • @Simon I'm not sure in this case how to identify (from the controller) the cells that are non-visible (and will pop back into the picture when re-use occurs) to call "setsNeedsDisplay" on them? I'll add some more detail in my question – Greg Oct 21 '15 at 19:33
  • the cellForRow:AtIndexPath: is called just before they becomes visible :) – Simon Oct 22 '15 at 13:08
  • cellForRow is a table view method no? Not a UICollectionView – Greg Oct 22 '15 at 21:18
  • you're right, it's called -cellForItemAtIndexPath: in a collectionView – Simon Oct 23 '15 at 07:52
  • but here you would still need to determine whether it's a cell that came from a previous orientation first, else if you trigger a redraw here you're doing it much more than necessary still...so still the same overall – Greg Oct 23 '15 at 10:25
  • You need to define your sizes in `sizeForItemAtIndexPath` delegate method of `UICollectionViewDelegateFlowLayout`. That is where you should calculate the size of your cells. Then in the `viewWillTransitionToSize` you need to invalidate your collectionview layout like this : `[self.collectionView.collectionViewLayout invalidateLayout];` All of this must be done within your view controller that contains the collection view – Marc-Alexandre Bérubé Oct 23 '15 at 13:17
  • Try calling `setNeedsDisplay ` inside `layoutSubviews` of your cell class, – deoKasuhal Oct 23 '15 at 19:43
  • @Marc-AlexandreBérubé I'm not using flowlayout here, it's a custom UICollectionViewLayout. – Greg Oct 23 '15 at 23:13
  • @deoKaushal This causes "drawRect" to be called unnessarily (from my testing) for the normal cases of cell reuse, where a forced redraw is not required. I'm just looking for a way to detect the cases of reuse whereby the cell was cached by not showing/visible, then a rotation occurred, and then it's pulled out to be reused and display. Just in this case I want to trigger the redraw – Greg Oct 23 '15 at 23:28

3 Answers3

5

I think I have it now here:

import UIKit

@IBDesignable class GCCalendarCell: UICollectionViewCell {
    var prevBounds : CGRect?

    override func layoutSubviews() {
        if let prevBounds = prevBounds {
            if !( (prevBounds.width == bounds.width) && (prevBounds.height == bounds.height) ) {
                self.setNeedsDisplay()
            }
        }
    }

    override func drawRect(rect: CGRect) {
        // Do Stuff
        self.prevBounds = self.bounds
    }

}

Noted this check didn't work in "prepareForReuse" as at this time the cell had not had the rotation applied. Seems to work in "layoutSubviews" however.

Greg
  • 34,042
  • 79
  • 253
  • 454
1

You can implement some kind of communication between the cells and the view controller holding the collection view ( protocol and delegate or passed block or even direct reference to the VC ). Then You can ask the view controller for rotation changes.

Its a bit messy, but if You have some kind of rotation tracking in Your view controller You can filter the setNeedsDisplay with a simple if statement.

Georgi Boyadzhiev
  • 1,351
  • 1
  • 13
  • 18
0

I had similar challenged updating cells that were already displayed and off the screen. While cycling through ALLL cells may not be possible - refreshing / looping through non-visible ones is. IF this is your use case - then read on. Pre - Warning - if you're adding this sort of code - explain why you're doing it. It's kind of anti pattern - but can help fix that bug and help ship your app albeit adding needless complexity. Don't use this in multiple spots in app.

Any collectionviewcell that's de-initialized (off the screen and being recylced) should be unsubscribed automatically.

Notification Pattern

let kUpdateButtonBarCell = NSNotification.Name("kUpdateButtonBarCell")

class Notificator {
     static func fireNotification(notificationName: NSNotification.Name) {
          NotificationCenter.default.post(name: notificationName, object: nil)
     }
}

extension UICollectionViewCell{
    func listenForBackgroundChanges(){
         NotificationCenter.default.removeObserver(self, name: kUpdateButtonBarCell, object: nil)
        NotificationCenter.default.addObserver(forName:kUpdateButtonBarCell, object: nil, queue: OperationQueue.main, using: { (note) in

            print( " contentView: ",self.contentView)

        })
    }
}



override func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
    let cell =  collectionView.dequeueReusableCellWithReuseIdentifier("die", forIndexPath: indexPath) as UICollectionViewCell
    cell.listenForBackgroundChanges()
    return cell
}


 // Where appropriate broadcast notification to hook into all cells past and present
 Notificator.fireNotification(notificationName: kUpdateButtonBarCell) 

Delegate Pattern

It's possible to simplify this.... an exercise for the reader. just do not retain the cells (use a weak link) - otherwise you'll have memory leaks.

johndpope
  • 5,035
  • 2
  • 41
  • 43