Reload UICollectionView header or footer?

2020-02-02 10:44发布

I have some data that is fetched in another thread that updates a UICollectionView's header. However, I've not found an efficient way of reloading a supplementary view such as a header or footer.

I can call collectionView reloadSections:, but this reloads the entire section which is unnecessary. collectionView reloadItemsAtIndexPaths: only seems to target cells (not supplementary views). And calling setNeedsDisplay on the header itself doesn't appear to work either. Am I missing something?

10条回答
\"骚年 ilove
2楼-- · 2020-02-02 10:56

Here's what I did to update only the section headers that are currently loaded in memory:

  • Add a weakToStrong NSMapTable. When you create a header, add the header as the weakly held key, with the indexPath object. If we reuse the header we'll update the indexPath.
  • When you need to update the headers, you can now enumerate the objects/keys from the NSMapTable as needed.

    @interface YourCVController ()
        @property (nonatomic, strong) NSMapTable *sectionHeaders;
    @end

    @implementation YourCVContoller

    - (void)viewDidLoad {
        [super viewDidLoad];
        // This will weakly hold on to the KEYS and strongly hold on to the OBJECTS
        // keys == HeaderView, object == indexPath
        self.sectionHeaders = [NSMapTable weakToStrongObjectsMapTable];
    }

    // Creating a Header. Shove it into our map so we can update on the fly
    - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
    {
        PresentationSectionHeader *header = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"presentationHeader" forIndexPath:indexPath];
        // Shove data into header here
        ...
        // Use header as our weak key. If it goes away we don't care about it
        // Set indexPath as object so we can easily find our indexPath if we need it
        [self.sectionHeaders setObject:indexPath forKey:header];
        return header;
    }

    // Update Received, need to update our headers
    - (void) updateHeaders {
        NSEnumerator *enumerator = self.sectionHeaders.keyEnumerator;
        PresentationSectionHeader *header = nil;
        while ((header = enumerator.nextObject)) {
            // Update the header as needed here
            NSIndexPath *indexPath = [self.sectionHeaders objectForKey:header];
        }
    }

    @end
查看更多
相关推荐>>
3楼-- · 2020-02-02 10:58

I just ran into the same problem, and I ended up looking up the view using its tag to edit a label:

UICollectionReusableView *footer = (UICollectionReusableView*)[self.collectionView viewWithTag:999];
UILabel *footerLabel = (UILabel*)[footer viewWithTag:100];

Like you said it is unnecessary to reload an entire section, which cancels out any animation on cells as well. My solution isn't ideal, but it's easy enough.

查看更多
你好瞎i
4楼-- · 2020-02-02 10:58

This question is very old but a simple way to do it is to just set a delay that covers the time your view is animating and disabling the animation while you update the view...usually a delete or insert takes about .35 seconds so just do:

 delay(0.35){
            UIView.performWithoutAnimation{
            self.collectionView.reloadSections(NSIndexSet(index: 1))  
            }
查看更多
家丑人穷心不美
5楼-- · 2020-02-02 11:02

You can also use (the lazy way)

collectionView.collectionViewLayout.invalidateLayout() // swift

[[_collectionView collectionViewLayout] invalidateLayout] // objc

More complex would be to provide a context

collectionView.collectionViewLayout.invalidateLayout(with: context) // swift

[[_collectionView collectionViewLayout] invalidateLayoutWithContext:context] // objc

You can then make a or configure the context yourself to inform about what should be updated see: UICollectionViewLayoutInvalidationContext

It has a function in there that you can override:

invalidateSupplementaryElements(ofKind:at:) // swift

Another option is (if you have already loaded the correct header/footer/supplementary view) and you only want to update the view with the new data than you can use one of the following functions to retrieve it:

supplementaryView(forElementKind:at:) // get specific one visibleSupplementaryViews(ofKind:) // all visible ones

Same goes for visible cells with visibleCells. The advantage of just getting the view and not reloading a view entirely is that the cells retains it state. This is espically nice with table view cells when they use swipe to delete/edit/etc since that state is lost after reloading the cell.

If you feel fanatic you can of course also write some extensions to retrieve only cells/supplementary views of a given kind using generics

if let view = supplementaryView(forType: MySupplementaryView.self, at: indexPath) {
    configure(view, at indexPath)
}

this assumes that you have a function that registers/dequeues views in example with their class name. I made a post about this here

查看更多
淡お忘
6楼-- · 2020-02-02 11:02

Here are two ways you could do it.

1. Create a mutable model to back the data that will eventually be available. Use KVO in inherited class of UICollectionReusableView to observe the changes and update the header view with the new data as it comes available.

[model addObserver:headerView
        forKeyPath:@"path_To_Header_Data_I_care_about"
           options:(NSKeyValueObservingOptionNew |
                    NSKeyValueObservingOptionOld)
           context:NULL];

then implement listener method in header view

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

2. add notification listener to the view and post a notification when the data has successfully come available. Downside is that this is application wide and not a clean design.

// place in shared header file
#define HEADER_DATA_AVAILABLE @"Header Data Available Notification Name"

// object can contain userData property which could hole data needed. 
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(headerDataAvailable:) name:HEADER_DATA_AVAILABLE object:nil];

[[NSNotificationCenter defaultCenter] postNotificationName:HEADER_DATA_AVAILABLE object:nil];
查看更多
小情绪 Triste *
7楼-- · 2020-02-02 11:07

My problem arose when frame sizes for the supplementary views changed upon invalidating the layout. It appeared that the supplementary views were not refreshing. It turns out they were, but I was building the UICollectionReusableView objects programmatically, and I was not removing the old UILabel subviews. So when the collection view dequeued each header view, the UILabels would pile up, causing erratic appearance.

The solution was to build each UICollectionReusableView completely inside the viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath method, starting by a) removing all subviews from the dequeued cell, then b) getting the frame size from the item's layout attributes to allow adding the new subviews.

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath 
{
     yourClass *header = (yourClass *)[collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"identifier" forIndexPath:indexPath];
     [[header viewWithTag:1] removeFromSuperview]; // remove additional subviews as required
     UICollectionViewLayoutAttributes *attributes = [collectionView layoutAttributesForSupplementaryElementOfKind:kind atIndexPath:indexPath];
     CGRect frame = attributes.frame;
     UILabel *label = [[UILabel alloc] initWithFrame: // CGRectMake based on header frame
     label.tag = 1;
     [header addSubview:label];
     // configure label
     return header;
}
查看更多
登录 后发表回答