UIScrollViewとUIStackViewで、UITableViewのハイライトを作る

UITableViewのようなレイアウトを作る際に、UIScrollViewとUIStackViewを使って作る場合もあるかと思います。

UITableViewは選択されたセルをハイライトすることができますが、UIScrollViewとUIStackViewの場合はそのままではハイライトができません。

今回はセルのハイライトを再現する方法を紹介します。

リポジトリはこちら。 StackViewCellHighlight

StackViewCellHighlight

全体の構成

全体のStoryboardは画像のようになっています。

Storyboard

UIScrollViewの中にUIStackViewを配置して、それぞれのCellに該当するViewを配置しています。

それぞれのCellの中には、カスタムViewのSelectableCellViewを配置しています。このSelectableCellViewが選択時のハイライト処理を実装しています。また、UIScrollViewUIButtonのイベントをキャンセルできるUIButtonCancelableScrollViewというカスタムクラスにしています。このスクロールビューを使うことで、セルの選択時にスクロールをした場合にスムーズにスクロールすることができます。

SelectableCellViewとSelectableButton

SelectableCellViewは内部にSelectableButtonを配置しています。

SelectableButtonにはsetSelected(_ selected: Bool, animated: Bool)関数を定義していて、セルの選択状態が変更された時に呼び出します。isSelectedの状態管理と背景色の処理をしています。isHighlightもオーバーライドしていて、isSelectedと合わせて背景色のハンドリングをしています。

SelectableButton

class SelectableButton: UIButton {

    override var isHighlighted: Bool {
        get {
            return super.isHighlighted
        }
        set {
            backgroundColor = (isSelected || newValue) ? UIColor.opaqueSeparator : UIColor.white
            super.isHighlighted = newValue
        }
    }

    func setSelected(_ selected: Bool, animated: Bool) {
        func update() {
            isSelected = selected
            backgroundColor = selected ? UIColor.opaqueSeparator : UIColor.white
        }
        if animated {
            UIView.animate(withDuration: 0.35, animations: update)
        } else {
            update()
        }
    }

}

SelectableCellViewでは、SelectableButtontouchDown, touchCancel, touchUpInsideにハンドラを設定しています。それぞれのイベントで選択状態をセットします。

class SelectableCellView: UIView {

    private lazy var button: SelectableButton = {
        let button = SelectableButton()
        return button
    }()

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
        button.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0).isActive = true
        button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
        button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true

        button.addTarget(self, action: #selector(touchDown(button:)), for: .touchDown)
        button.addTarget(self, action: #selector(cancel(button:)), for: .touchCancel)
        button.addTarget(self, action: #selector(handleTap(button:)), for: .touchUpInside)
    }

    var didTap: (() -> Void)?

    @objc private func handleTap(button: SelectableButton) {
        setSelected(true, animated: false)
        didTap?()
    }

     @objc private func touchDown(button: SelectableButton) {
        setSelected(true, animated: false)
    }

     @objc private func cancel(button: SelectableButton) {
        setSelected(false, animated: false)
    }

     func setSelected(_ selected: Bool, animated: Bool) {
        button.setSelected(selected, animated: animated)
    }
}

これでUIStackViewに配置したViewがタップされた時に背景がハイライトされるようになります。

UITableViewControllerには、セルをタップした時にセルを選択してプッシュ遷移した後、画面に戻ってきたらハイライトを消すclearsSelectionOnViewWillAppearという設定があります。

これを実現するには、選択されたセルを保持して、ViewControllerのviewWillAppearで適切に選択状態をハンドリングする必要があります。

class ViewController: UIViewController {

    ....

    var selectedCell: SelectableCellView?
    var clearsSelectionOnViewWillAppear: Bool = true

    ....

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if let selectedCell = selectedCell, clearsSelectionOnViewWillAppear {
            transitionCoordinator?.animate(alongsideTransition: { _ in
                selectedCell.setSelected(false, animated: animated)
            }, completion: { context in
                if context.isCancelled {
                    selectedCell.setSelected(true, animated: false)
                } else {
                    self.selectedCell = nil
                }
            })
        }
    }
}

これで、UITableViewのハイライトをUIScrollViewUIStackViewで実現することができました。

こちらにサンプルコードを置いてあります。

StackViewCellHighlight

エンジニアリング UIStackView UIScrollView UITableView ハイライト


関連記事