Search code examples
iosswifttvosapple-tv

tvOS SearchController, avoid collapsing keyboard


I created a SearchController in tvOS, which shows the results in a CollectionView with horizontal scroll.

The final result looks quite horrible because the keyboard automatically dismisses when the focus moves from the keyboard to the results.

Because the height of the collection view is larger without the keyboard, the elements on it are automatically realigned in the collection view. That makes the UX very confusing.

You can see the problem in the following GIF. Moving down the focus from the keyboard to the results, ends up even focusing a non expected element. ("Seven" instead of "Five")

enter image description here

Is there any way to avoid that the keyboard collapses?. I noticed that when the results view contains a non scrollable view, the keyboard doesn't collapse, but I need to make my results scrollable.

Here you can find a code reproducing the issue.

import UIKit

private var items = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"]

final class MySearchViewController: UIViewController, UICollectionViewDataSource {

    override func viewDidLoad() {
        super.viewDidLoad()
        setUpView()
    }

    // MARK: - Private

    private lazy var searchContainerViewController: UISearchContainerViewController = {
        return UISearchContainerViewController(searchController: searchController)
    }()

    private lazy var searchController: UISearchController = {
        let searchController = UISearchController(searchResultsController: searchResultsController)
        return searchController
    }()

    private lazy var searchResultsController: UIViewController = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal

        let searchResultsController = UICollectionViewController(collectionViewLayout: layout)
        searchResultsController.collectionView.dataSource = self
        MyCell.register(in: searchResultsController.collectionView)
        return searchResultsController
    }()

    private func setUpView() {
        embed(viewController: searchContainerViewController, inContainerView: view)
    }

    // MARK: - 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
    }
}

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)
    }
}

A workaround to avoid focusing a wrong element could be to assign a fixed height to the CollectionView. But doing that, still hides the keyboard, leaving a lot of redundant empty space on the screen.

Since hiding the keyboard is useless in this situation. I would like to keep it always visible.

This is the result of using a fixed height for the collection view.

enter image description here


Solution

  • It was not really possible until tvOS 14, when Apple released new property searchControllerObservedScrollView: https://developer.apple.com/documentation/uikit/uisearchcontroller/3584820-searchcontrollerobservedscrollvi

    By setting your collectionView on this property, the SearchController will automatically adapt searchbar + keyboard position to your CollectionView offset.

    You can adapt your code this way :

        private lazy var searchController: UISearchController = {
            let searchController = UISearchController(searchResultsController: searchResultsController)
            if #available(tvOS 14.0, *) {
                searchController.searchControllerObservedScrollView = (searchResultsController as? UICollectionViewController)?.collectionView
            }
            return searchController
        }()
    

    To get rid of nullable cast, just change searchResultsController return type to UICollectionViewController. It should not affect your implementation.