🐵

[UIKit] 画面表示前にscrollToItem(at:at:animated:)したい!

2022/12/19に公開

Swift/Kotlin愛好会アドベントカレンダー19日目の記事です。
https://qiita.com/advent-calendar/2022/love_swift_kotlin

はじめに

CollectionViewが載っている画面を表示前に指定箇所へスクロールする際に少し工夫が必要だったので、自分なりの実装を書いておきます。

画面構成

A画面:B画面に遷移するボタンがある
B画面:CollectionViewが載っている

実装

AViewController
class AViewController: UIViewController {

    @IBOutlet weak var button: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //B画面へIndexPathを渡して遷移するアクションを追加
        button.addAction(UIAction(handler: { _ in
            let bVC = self.storyboard!.instantiateViewController(identifier: "B") { corder in
                return BViewController(coder: corder, initialIndexPath: IndexPath(row: 20, section: 0))
            }
            self.present(bVC, animated: true)
        }), for: .touchUpInside)
    }
    
}
BViewController
class BViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    
    private var initialIndexPath: IndexPath?
    
    init?(coder: NSCoder, initialIndexPath: IndexPath) {
        self.initialIndexPath = initialIndexPath
        super.init(coder: coder)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        //後に説明
        if let initialIndexPath = initialIndexPath {
            collectionView.scrollToItem(at: initialIndexPath, at: .centeredVertically, animated: false)
            guard let initialCell = collectionView.cellForItem(at: initialIndexPath) else { return }
            if collectionView.visibleCells.contains(where: { $0 == initialCell }) {
                self.initialIndexPath = nil
            }
        }
    }
    
}

説明

まずscrollToItem(at:at:animated:)ですが、この子はviewDidLayoutSubviews()以降のライフサイクルでのみ動作するようなので[1]viewDidLayoutSubviews()内で呼び出してあげています。
viewDidAppear(_:)でも動作するのですが、表示されてからスクロール処理が走る関係上見た目があまりよくないのでやめました。

またviewDidLayoutSubviews()は画面初期表示時に複数回呼ばれるのですが、
最初の1、2回目の呼び出しではCollectionViewのレイアウトが完了しておらず、
スクロールが行われない場合があるため、スクロールされたかどうかを判定する処理を追加しています。

guard let initialCell = collectionView.cellForItem(at: initialIndexPath) else { return }
if collectionView.visibleCells.contains(where: { $0 == initialCell }) {
    self.initialIndexPath = nil
}

スクロールが正常に行われた場合はinitialIndexPathnilを代入して2度目以降のスクロールが起きないようにしています。

おわりに

上記の処理を利用したアプリをリリースしているのでよかったら使ってみてください:)
https://apple.co/3Uuyzhc

参考リンク

scrollToItem(at:at:animated:)
viewDidLayoutSubviews()

脚注
  1. ちゃんと確認したわけではないので間違っているかもしれないです。。。 ↩︎

Discussion