Как вручную выбрать путь следующего сфокусированного индекса в представлении коллекции

Как я могу вручную выбрать следующий путь индексации для моей коллекции в tvOS?

Мой вариант использования заключается в том, что у меня есть представление коллекции с динамическими ячейками, которые прокручиваются во всех направлениях, и иногда, когда ячейка в одном разделе охватывает несколько ячеек в следующем, я хочу иметь возможность вручную решить, какая ячейка будет сфокусирована следующей.

По-видимому, поведение по умолчанию таково, что представление коллекции выберет ячейку посередине предыдущей.

Например, зеленая ячейка в данный момент находится в фокусе. Когда я двигаюсь вниз, представление коллекции хочет сфокусировать красную ячейку, но я хочу, чтобы синяя ячейка была сфокусирована следующей.

Моя первоначальная идея заключалась в том, чтобы реализовать collectionView(_:shouldUpdateFocusIn:) делегировать функцию и вернуть false + запускать обновление фокуса, когда представление коллекции выбирает "неправильный" путь индекса.

Однако по какой-то причине shouldUpdateFocusIn звонят несколько раз, когда я возвращаюсь false от него и вызывает видимое отставание.

       func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
    if let nextIndexPath = shouldFocusCell(context.focusHeading, (context.previouslyFocusedIndexPath, context.nextFocusedIndexPath)) {
        // The collection view did not select the index path of the cell we want to focus next.
        // Save the correct index path and trigger a focus update.
        lastFocusChange = (lastFocusChange.next, nextIndexPath)
        setNeedsFocusUpdate()
        // Using updateFocusIfNeeded() results in the following warning:
        // WARNING: Calling updateFocusIfNeeded while a focus update is in progress. This call will be ignored.
        return false
    }
    return true
}

Следующей идеей было сделать то же самое в collectionView(_:didUpdateFocusIn:with:), но в этом случае мы обновляем фокус только после того, как он уже переместился в "неправильную" ячейку, поэтому пользователю становится очевидно, что фокус перемещается с неправильной ячейки на правильную.

Тоже не идеально.

Я использую свой собственный подкласс UICollectionViewLayout и UICollectionView, но я не вижу ничего, что можно было бы переопределить, чтобы иметь возможность вручную решить, какой путь индекса фокусировать следующим при перемещении вверх / вниз / влево / вправо перед shouldUpdateFocusIn называется.

Есть ли способ добиться этого?

2 ответа

Мы можем добиться этого, используя метод протокола делегата UICollectionView, пожалуйста, найдите фрагмент кода.

класс ViewController: UIViewController {

      @IBOutlet weak var sideMenuTableView: UITableView!
@IBOutlet weak var collectionView: UICollectionView!
let menueItem = ["Home", "Sports", "Movies", "Listen", "Play", "Game"]
let colors: [UIColor] = [.green, .blue, .purple, .orange, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue]
var lastFocusedIndexPath: IndexPath?

override func viewDidLoad() {
    super.viewDidLoad()
    lastFocusedIndexPath = IndexPath(row: 2, section: 0)
    initialConfiguration()
}

override var preferredFocusEnvironments : [UIFocusEnvironment] {
    return [collectionView]
}

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    updateTableViewContentInset()
}
    
func updateTableViewContentInset() {
    self.sideMenuTableView.contentInset = UIEdgeInsets(top: 0, left: -40, bottom:  0, right: 0)
}

func initialConfiguration() {
    sideMenuTableView.register(UINib.init(nibName: "SideMenuTVCell", bundle: nil), forCellReuseIdentifier: "SideMenuTVCell")
    sideMenuTableView.delegate = self
    sideMenuTableView.dataSource = self
    sideMenuTableView.backgroundColor = .yellow
    sideMenuTableView.isHidden = false
    
    collectionView.register(UINib.init(nibName: "ColorCVCell", bundle: nil), forCellWithReuseIdentifier: "ColorCVCell")
    collectionView.delegate = self
    collectionView.dataSource = self
    collectionView.backgroundColor = .white
}

}

extension ViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, раздел numberOfRowsInSection: Int) -> Int {return menueItem.count}

      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "SideMenuTVCell", for: indexPath) as! SideMenuTVCell
    cell.configureCell(string: menueItem[indexPath.row])
    return cell
}

}

extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView (_ collectionView: UICollectionView, раздел numberOfItemsInSection: Int) -> Int {return colors.count}

      func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCVCell", for: indexPath) as! ColorCVCell
    cell.backgroundColor = colors[indexPath.row]
    cell.configureCell(color: colors[indexPath.row])
    return cell
}


func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
    if let previousIndexPath = context.previouslyFocusedIndexPath,
       let cell = collectionView.cellForItem(at: previousIndexPath) {
        cell.contentView.layer.borderWidth = 0.0
        cell.contentView.layer.shadowRadius = 0.0
        cell.contentView.layer.shadowOpacity = 0
    }

    if let indexPath = context.nextFocusedIndexPath,
       let cell = collectionView.cellForItem(at: indexPath) {
        cell.contentView.layer.borderWidth = 8.0
        cell.contentView.layer.borderColor = UIColor.black.cgColor
        cell.contentView.layer.shadowColor = UIColor.black.cgColor
        cell.contentView.layer.shadowRadius = 10.0
        cell.contentView.layer.shadowOpacity = 0.9
        cell.contentView.layer.shadowOffset = CGSize(width: 0, height: 0)
    }
            
    if let indexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
        UIView.animate(withDuration: 0.3) { () -> Void in
            cell.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        }
    }

    if let indexPath = context.nextFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
        UIView.animate(withDuration: 0.3) { () -> Void in
            cell.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
        }
    }
}

func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
    if let previouslyFocusedIndexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: previouslyFocusedIndexPath) {
        let collectionViewWidth = collectionView.frame.width
        let cellWidth = cell.frame.width
        let rowCount = Int(ceil(collectionViewWidth / cellWidth))
        let remender = previouslyFocusedIndexPath.row % rowCount
        let nextIndex = previouslyFocusedIndexPath.row - remender + rowCount
        if let nextFocusedInndexPath = context.nextFocusedIndexPath {
            if context.focusHeading == .down {
                moveFocus(to: IndexPath(row: nextIndex, section: 0))
                return true
            }
        }
    }
    return true
}

private func moveFocus(to indexPath: IndexPath) {
    lastFocusedIndexPath = indexPath
    print(collectionView.indexPathsForVisibleItems)
    DispatchQueue.main.async {
        self.setNeedsFocusUpdate()
        self.updateFocusIfNeeded()
    }
}

func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
    return lastFocusedIndexPath
}

}

Одна из возможностей - использовать collectionView(_ collectionView:, canFocusItemAt:) чтобы ваш collectionView знал, может ли данный indexPath получить фокус.

Ниже вы можете найти наивную реализацию этой концепции. Вам нужно будет скорректировать математику на нем в соответствии с вашими потребностями.

func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
    guard let currentlyFocusedCellLayoutAttributes = collectionView.layoutAttributesForItem(at: focusedIndexPath) else { return false }
    guard let cellAtGivenIndexPathLayoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return false }
        
    let currentlyFocusedCellOriginX = currentlyFocusedCellLayoutAttributes.frame.origin.x
    let currentlyFocusedCellOriginY = currentlyFocusedCellLayoutAttributes.frame.origin.y
    let currentlyFocusedCellWidth = currentlyFocusedCellLayoutAttributes.frame.width
        
    let cellAtGivenIndexPathOriginX = cellAtGivenIndexPathLayoutAttributes.frame.origin.x
    let cellAtGivenIndexPathOriginY = cellAtGivenIndexPathLayoutAttributes.frame.origin.y
    let cellAtGivenIndexPathWidth = cellAtGivenIndexPathLayoutAttributes.frame.width
            
    let offsetX = collectionView.contentOffset.x

    // Scrolling horizontally is always allowed
    if currentlyFocusedCellOriginY == cellAtGivenIndexPathOriginY {
        return true
    }
    
    // Scrolling vertically is only allowed to the first cell (blue cell in the screenshot)
    if cellAtGivenIndexPathOriginX <= offsetX {
        return true
    }
    
    return false
}
Другие вопросы по тегам