indexPathForPreferredFocusedView is not being Call

2019-06-25 13:29发布

问题:

I need to specify which cell should receive focus.

According to Apple Documentation the indexPathForPreferredFocusedView delegate method should be called if the remembersLastFocusedIndexPath property is false, or if there is no saved index path because no cell was previously focused.

In my case I am using a collection view in a UIViewController and setting remembersLastFocusedIndexPath to false but indexPathForPreferredFocusedView is not being called.

How explain this behaviour?

回答1:

The function indexPathForPreferredFocusedView is part of UICollectionViewDelegate, so it might be that the delegate was not assigned to your collectionView.

Or, the problem might also be on the focus environment, not taking into account your UICollectionView.

As a reference, here you have an example of a simple collectionView with 5 cells, having the one in the center initially selected by default

import UIKit

class ViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var items = ["One", "Two", "Three", "Four", "Five"]

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

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
        collectionView.remembersLastFocusedIndexPath = false
        MyCell.register(in: collectionView)
        view.addSubview(collectionView)

        collectionView.dataSource = self
        collectionView.delegate = self
    }
}

extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCell.reuseIdentifier, for: indexPath) as! MyCell
        cell.titleLabel.text = items[indexPath.row]
        return cell
    }
}

extension ViewController: UICollectionViewDelegate {

    func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
            return IndexPath(row: 2, section: 0)
    }
}

class MyCell: UICollectionViewCell {

    static var reuseIdentifier: String { return String(describing: self) + "ReuseIdentifier" }

    var titleLabel: UILabel!

    public static func register(in collectionView: UICollectionView) {
        collectionView.register(MyCell.self, forCellWithReuseIdentifier: MyCell.reuseIdentifier)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        titleLabel = UILabel(frame: bounds)
        backgroundColor = .blue
        contentView.addSubview(titleLabel)
    }

    override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
        backgroundColor = isFocused ? .red : .blue
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}



回答2:

Swift 4: I had the same issue. I wanted to reload collectionview and force focus on a particluar cell. If I set collectionView.remembersLastFocusedIndexPath = true, indexPathForPreferredFocusedView delegate method is called. But it will remember the last focused indexPath as the property name says. This is not what I wanted. Apple docs are confusing.

Try this:

  1. Implement the delegate method:

    func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
    return IndexPath(item: 5, section: 0)
    }
    
  2. Force update the focus [ex: in a button action]:

    collectionView.setNeedsFocusUpdate()
    collectionView.updateFocusIfNeeded()
    

This will force the collectionView to call the indexPathForPreferredFocusedView method. We don't have to set the remembersLastFocusedIndexPath to true.

NB: I had multiple collectionViews in my screen and the above steps will work only if the currently focused collectionView and the collectionView which we are forcing the focus update are same.

Use the preferredFocusEnvironments to return the preferred collectionview to be in focus when the view loads [for example].

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


回答3:

From what I have tried, indexPathForPreferredFocusedView is called only when the collectionView.remembersLastFocusedIndexPath is set to true.

Also, when the collectionView is reloaded again to get the preferred Focus.

Using preferredFocusEnvironments is also a good solution, but it has its own cons. If your ViewController is only having collectionView, then it is good to be used. If there are multiple focus items within the ViewController, the focus behaviour will be different.



回答4:

Three things that solved the same problem I was facing.

  1. self.restoreFocusAfterTransition = false

  2. func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> 
        IndexPath? {
            return IndexPath(item: 3, section: 0)
        }
    
  3. override var preferredFocusEnvironments : [UIFocusEnvironment] {
            //return collectionView in order for indexPathForPreferredFocusedView method to be called.
        return [collectionView]
    }
    

I had two collection view in two different view controllers. I had subclassed the UICollectionView and was performing a transition from one view controller to another.