Search code examples
uicollectionviewfocustvosapple-tv

Custom focus engine behaviour for UICollectionView


I use a standard UICollectionView with sections. My cells are laid out like a grid. The next cell in a chosen direction is correctly focused if the user moves the focus around with the Apple TV remote. But if there is a "gap" in the grid, the default focus engine jumps over sections. It does this to focus a cell which could be several sections away but is in the same column.

Simple Example: There are 3 sections. The first section has 3 cells. The second has 2 cells and the last one has 3 cells again. See the following image:

enter image description here

If the green cell is focused and the user touches the down direction, the yellow cell gets focused and section two is skipped by the focus engine.

I would like to force it that no sections can get jumped over. So instead of focusing the yellow cell I would like to focus the blue cell.

I learned that the Apple TV Focus engine internally works like a grid system and that the described behaviour is the default one. To allow other movements (e.g. diagonal) we need to help the focus engine by placing invisible UIFocusGuides which can redirect the focus engine to a preferredFocusedView.

So in the following image there is one invisible red focus guide placed into the empty space of a UICollectionView section which would redirect the down focus to the desired blue cell. I think that would be the perfect solution, in theorie.

enter image description here

But how would I add UIFocusGuides to all empty spaces of UICollectionView sections? I have tried several things but nothing worked. Maybe add it as a Decorator View but that seems wrong. Or as additional cells, but that breaks the data layer and the constraints anchors do not work.

Has anyone an idea on how to add UIFocusGuides to a UICollectionView?


Solution

  • One solution is to subclass UICollectionViewFlowLayout with a layout that adds Supplementary Views with UIFocusGuides on top for all empty areas.

    Basically the custom flow layout calculates the needed layout attributes in the prepareLayout like this:

    self.supplementaryViewAttributeList = [NSMutableArray array];
    if(self.collectionView != nil) {
        // calculate layout values
        CGFloat contentWidth = self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right;
        CGFloat cellSizeWithSpacing = self.itemSize.width + self.minimumInteritemSpacing;
        NSInteger numberOfItemsPerLine = floor(contentWidth / cellSizeWithSpacing);
        CGFloat realInterItemSpacing = (contentWidth - (numberOfItemsPerLine * self.itemSize.width)) / (numberOfItemsPerLine - 1);
    
        // add supplementary attributes
        for (NSInteger numberOfSection = 0; numberOfSection < self.collectionView.numberOfSections; numberOfSection++) {
            NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:numberOfSection];
            NSInteger numberOfSupplementaryViews = numberOfItemsPerLine - (numberOfItems % numberOfItemsPerLine);
    
            if (numberOfSupplementaryViews > 0 && numberOfSupplementaryViews < 6) {
                NSIndexPath *indexPathOfLastCellOfSection = [NSIndexPath indexPathForItem:(numberOfItems - 1) inSection:numberOfSection];
                UICollectionViewLayoutAttributes *layoutAttributesOfLastCellOfSection = [self layoutAttributesForItemAtIndexPath:indexPathOfLastCellOfSection];
    
                for (NSInteger numberOfSupplementor = 0; numberOfSupplementor < numberOfSupplementaryViews; numberOfSupplementor++) {
                    NSIndexPath *indexPathOfSupplementor = [NSIndexPath indexPathForItem:(numberOfItems + numberOfSupplementor) inSection:numberOfSection];
                    UICollectionViewLayoutAttributes *supplementaryLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:ARNCollectionElementKindFocusGuide withIndexPath:indexPathOfSupplementor];
                    supplementaryLayoutAttributes.frame = CGRectMake(layoutAttributesOfLastCellOfSection.frame.origin.x + ((numberOfSupplementor + 1) * (self.itemSize.width + realInterItemSpacing)), layoutAttributesOfLastCellOfSection.frame.origin.y, self.itemSize.width, self.itemSize.height);
                    supplementaryLayoutAttributes.zIndex = -1;
    
                    [self.supplementaryViewAttributeList addObject:supplementaryLayoutAttributes];
                }
            }
        }
    }
    

    and then returns the needed layouts in the layoutAttributesForSupplementaryViewOfKind: method like this:

    if ([elementKind isEqualToString:ARNCollectionElementKindFocusGuide]) {
        for (UICollectionViewLayoutAttributes *supplementaryLayoutAttributes in self.supplementaryViewAttributeList) {
            if ([indexPath isEqual:supplementaryLayoutAttributes.indexPath]) {
                layoutAttributes = supplementaryLayoutAttributes;
            }
        }
    }
    

    Now your Supplementary Views just need a UIFocusGuide the same size as the supplementary view itself. That's it.

    A full implementation of the method described can be found here on gitHub