简体   繁体   中英

UICollectionView calling setNeedsDisplay on UICollectionViewCells

I have a UICollectionView with a custom UICollectionViewCell class, in which I draw the necessary contents. I'm finding however that as I scroll the collection view up and down, it is redrawing every cell as it scrolls off and back on to the screen again. This results in quite a jerky scrolling. I thought that the UICollectionViewCells were automatically cached and reused, so I don't understand why it is constantly redrawing them.

I have an NSLog statement in the setNeedsDisplay of the subview of my custom UICollectionViewCell which is why I know it's redrawing them. Here is my UICollectionView code for drawing the info. (The PFObject is just an object retrieved from a database using Parse)

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MiniCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MiniCell" forIndexPath:indexPath];

    if (!cell.mini) {
        PFObject *pfMini = [_searchResults objectAtIndex:indexPath.row];
        Mini *mini = [Mini instanceWithPFObject:pfMini];
        cell.mini = mini;
        cell.backgroundColor = [UIColor grayColor];
    }
    return cell;
}

And my custom cell. The MiniView here is a custom view that I've created to display images, and I use it in various other places:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    MiniView *miniView = [[MiniView alloc] initWithFrame:rect];
    miniView.mini = _mini;
    [self addSubview:miniView];

    UIImageView *backgroundImage = [[UIImageView alloc] initWithImage:[self randomBackground]];
    self.backgroundView = backgroundImage;
}

- (UIImage *)randomBackground
{
    int backgroundInt = arc4random()%26 + 8;
    UIImage *background = [UIImage imageNamed:[NSString stringWithFormat:@"MainBackground%i", backgroundInt]];
    return background;
}

You should be placing your initializing statements inside the initWithFrame: method.

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        MiniView *miniView = [[MiniView alloc] initWithFrame:rect];
        miniView.mini = _mini;
        [self addSubview:miniView];

        UIImageView *backgroundImage = [[UIImageView alloc] initWithImage:[self randomBackground]];
        self.backgroundView = backgroundImage;
    }
    return self;
}

I cannot remember initializing being done in the drawRect: method without actually drawing content. If you would like to actually draw the cell's content, you should be using a graphics context:

UIGraphicsBeginImageContext(self.bounds.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *imageOfContextSnapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

This will take a "snapshot" of the current context. You can think of it as pasting the objects to a canvas. Calling this in your drawRect: method should draw your contents. Note that you should only be updating the context in the drawRect: method, otherwise you will receive and error. Additionally, only using the drawRect: method if you will actually be calling drawing methods. When creating a new UIView class, the drawRect: method even states that you should only use the method if you will be drawing content.

For more information, checkout Apple's reference. They have a very thorough explanation of the graphics context and how to use it.

https://developer.apple.com/library/mac/documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_context/dq_context.html

EDIT 1:

You may want to consider not drawing anything depending on the number of cells. The dequeueReusableCellWithIdentifier already recycles cell content and does a very good job of it. You should be adding complexity in parallel with necessity. Though that is easy for me to say because I do not know what MiniView is/is doing.

Additionally, if you want better performance you may want to consider object-specific drawing methods rather than using renderInContext:. For example, NSSString has:

-drawAtPoint:forWidth:withFont:
-drawAtPoint:forWidth:withFont:

These would be better performers for drawing text than to render the whole context. To draw an image you should call:

-drawInRect:

These must be called in a graphics context, in your case in the drawRect: method.

Edit 2:

It does not draw the mini when initializing in the init method because when you call

PFObject *pfMini = [_searchResults objectAtIndex:indexPath.row];
Mini *mini = [Mini instanceWithPFObject:pfMini];
cell.mini = mini;
cell.backgroundColor = [UIColor grayColor];

in your view controller, the mini view has been initialized and added to the cell as a subView, but the cell's mini iVar was most likely nil when the MiniView's mini iVar was assigned. In other words, when you call this in the initWithFrame:(CGRect)frame method:

MiniView *miniView = [[MiniView alloc] initWithFrame:rect];
miniView.mini = _mini;
[self addSubview:miniView];

the "_mini" is nil at that point. In your cellForItemAtIndexPath: method, you are then assigning the cell's mini, but not the MiniView's mini, thus the MiniView's mini continues as nil. You could assign the MiniView's mini in the cellForItemAtIndexPath: rather than the cell's mini and that should solve it.

Your cells should represent some data that lives in your model .

Right now I see that you create a random background for each cell, but cells can be created and destroyed as necessary, so you really should store this information in some place.

Once you decide that backgrounds are stored in, for example, @property NSArray *backgrounds of your view controller, you can use the standard pattern:

@interface MyCell : UITableViewCell

@property NSString *backgroundName; 
@property (weak) MiniView *miniView;
...

@implementation MyCell

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier] ) {
        MiniView *miniView = [[MiniView alloc] initWithFrame:rect];
        [self addSubview:miniView];
        self.miniView = miniView;
      // continue creating all necessary subviews ...
}

- (void)setBackgroundName:(NSString *)name {
    // assign background 
}

Then when you dequeue the cell, you can use cell.backgroundName = backgrounds[index] .

UICollectionView caches and reuses only visible cells for example 6 of then even if you have 100. So when you are scrolling it takes cells that went offscreen and deques then as new ones.

If you want to hold all cells in memory you may create cells

MyCell *cell = [[MyCell alloc] initWithFrame:CGRectMake(...)];

and store them in internal array (for each row) and when delegate ask for already created cell you gave him the one from array.This solution will consume more memory because all cells will be in memory.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM