Custom focus engine behaviour for UICollectionView
Asked Answered
S

3

8

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?

Schuck answered 6/12, 2015 at 17:43 Comment(2)
Same problem for me. Have you tried to override collectionView:canFocusItemAtIndexPath and return false to yellow cell?Vitovitoria
No I have not. But I guess that could create a little mess, cause the cells should be focusable. I still prefer the UIFocusGuide technique. But still do not know how to achieve it. I asked in the apple forums too: forums.developer.apple.com/message/93020 Please inform me if you found a solution.Schuck
S
-1

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

Schuck answered 12/8, 2016 at 14:22 Comment(0)
P
0

Two ways to achieve your goal. At least what I tried and made it work. =). There might be other ways too.

1:

The easiest way to achieve your goal is add vertical collection view with cells that contains horizontal collection view. Each horizontal collection is your section.

Make sure you added the following code to the vertical collection view:

func collectionView(collectionView: UICollectionView, canFocusItemAtIndexPath indexPath: NSIndexPath) -> Bool {
    return false
}

2:

If you want to use UIFocusGuide I think a good place to add focus guide is section header. Make sure your focus guide in section header and update preferredFocusedView as switch to each section. In my case I assign the first cell of each section when I leave the section.

Pentalpha answered 27/5, 2016 at 21:38 Comment(0)
I
0

Maybe you could try to implement optional func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? method, which is part of UICollectionViewDelegate?

Implements this method for your Section 1, 2, 3... UICollectionView and returns, let's say IndexPath(item: 0, section: 0) if you want the focus engine to automatically focus the first item.

Note that, if you would want to also set sectionCollectionView.remembersLastFocusedIndexPath= true

Integumentary answered 20/8, 2019 at 9:59 Comment(0)
S
-1

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

Schuck answered 12/8, 2016 at 14:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.