0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Swift】CollectionViewCellでセルの範囲選択処理を実装してみた

Posted at

前書き

こちらの記事を参考にCollectionViewCellでセルの範囲選択処理を実装しようと思ったのですが、以下のような不都合な点がいくつかあったので、こちらの記事も参考にしつつ改造してみました。

  • 元ネタの記事にそもそも「実用に致命的な課題」という部分があり解決されずに放置されていた(多分)。
    (今回は特に以下の課題を解決しました。)
    • タッチイベントが競合してスクロールが効かない
    • スクロールした先の座標の計算を考慮してない
    • カラムが4列の場合しか考慮してない
  • 実装されていた処理がAppleの写真等を範囲選択する場合の処理と挙動が違っていました。
  • 記事が古く現時点(2022年9月時点)のSwiftに対応していない。

完成イメージ

あとで編集して上げたいと思います。

本題

現在のソース全部はこちらにございます。

ViewController内の以下の部分を設定したら使えるようになるはずです。

ViweController.swift(10-15行目付近と40行目付近)
class ViewController: UIViewController {
    // 特に設定する部分
    var data = [String]()
    let numOfRow:CGFloat = 5
    
    @IBOutlet var collectionView: CollectionView!
// -----省略------
        for i in 1...150{
            data.append( String(i) )
        }
// -----省略------
}

data変数に表示させたいデータをまとめたデータを入れてください。
numOfRow変数にカラムの列数を入れてください。
40行目付近のループを消してください。

もう少し詳しく解説

主要な部分を抜粋して説明していきます。

CollwctionView

まず、オリジナルのdelegateを追加して、touchesBegan、touchesMoved、touchesEndedのイベントをViewControllerに送ります。
touchsもViewControllerに送ることで、「スクロールした先の座標の計算を考慮してない」の部分を解決しました。

CollectionView.swift (10行目付近[オリジナルのデリゲートを追加])
protocol AdditionalDelegate{
    func selectTouchBeganCell(touches: Set<UITouch>, beganPoint: CGPoint?)
    func selectTouchRangeCells(touches: Set<UITouch>, beganPoint: CGPoint?, movedPoint: CGPoint?)
    func selectTouchEndedCells()
}
CollectionView.swift (25行目付近[touchesBeganの編集])
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        self.touchesBeganPoint = touches.first?.location(in: self)
        
        // タップした座標のセルの選択ステータスを変更する
        self.additionalDelegate?.selectTouchBeganCell(touches: touches, beganPoint: touchesBeganPoint)
    }
CollectionView.swift (35行目付近[touchesMovedの編集])
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        
        if let locationPoint = touches.first?.location(in: self) {
            self.touchesMovedPoint = locationPoint
        }
        
        // タップした範囲内のセルの選択ステータスを変更する
        self.additionalDelegate?.selectTouchRangeCells(touches: touches, beganPoint: touchesBeganPoint, movedPoint: touchesMovedPoint)
    }
CollectionView.swift (50行目付近[touchesEndedの編集])
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        self.touchesBeganPoint = nil
        self.touchesMovedPoint = nil
        self.previewRangeSelectedIndexPathes.removeAll()
        
        // タッチイベントが終了したので、スクロールを可能に戻す
        self.isScrollEnabled = true
        
        // 範囲選択表示を削除する
        self.additionalDelegate?.selectTouchEndedCells()
    }

ViewController

続いて、ViewControllerでdelegateの処理部分と、それに必要な設定をします。

まずはdelegateの処理部分です。
後述する自動スクロールの発火もこの部分で行います。

ViewController.swift (50行目付近-120行目付近)
extension ViewController: AdditionalDelegate{
    func selectTouchEndedCells() {
        DispatchQueue.main.async {
            self.shouldFinish = true
            self.stopAutoScrollIfNeeded()
        }
    }
    
    func selectTouchBeganCell(touches: Set<UITouch>, beganPoint: CGPoint?) {
        if let indexPath = self.collectionView.indexPathForItem(at: beganPoint!) {
            self.collectionView.previewRangeSelectedIndexPathes.append(indexPath)
            updateCollectionView(indexPath)
            cachedSelectedIndices = selectedIndices
        }
    }
    
    /**
     移動した座標間に含まれるセルの選択ステータスを変更する
     - paremeter beganPoint: 選択を開始した座標
     - parameter movedPoint: 選択範囲の終端の座標
     */
    func selectTouchRangeCells(touches: Set<UITouch>, beganPoint: CGPoint?, movedPoint: CGPoint?) {
        if let locationPoint = touches.first?.location(in: self.view){
            let timeInterval = 0.1
            if(locationPoint.y <= scrollStartHeightTop){
                // 上にスクロールさせる
                collectionView.isScrollEnabled = true
                startAutoScroll(duration: timeInterval, direction: "up")
            } else if(scrollStartHeightBottom <= locationPoint.y){
                // 下にスクロールさせる
                collectionView.isScrollEnabled = true
                startAutoScroll(duration: timeInterval, direction: "down")
            } else{
                collectionView.isScrollEnabled = false
                stopAutoScrollIfNeeded()
            }
        }
        // 座標内に含まれるセルを取得する
        if let beganPoint = beganPoint, let movedPoint = movedPoint{
            if let beganIndexPath = collectionView.indexPathForItem(at: beganPoint), let movedIndexPath = collectionView.indexPathForItem(at: movedPoint){
                updateSelectedIndices = []
                for item in min(beganIndexPath.item, movedIndexPath.item)...max(beganIndexPath.item, movedIndexPath.item){
                    updateSelectedIndices.append(item)
                }
                selectedIndices = []
                if(updateBooleanValue){
                    for i in cachedSelectedIndices{
                        selectedIndices.append(i)
                    }
                    for i in updateSelectedIndices {
                        if( !selectedIndices.contains(i) ){
                            selectedIndices.append(i)
                        }
                    }
                } else{
                    for i in cachedSelectedIndices{
                        if( !updateSelectedIndices.contains(i) ){
                            selectedIndices.append(i)
                        }
                    }
                }
                collectionView.reloadData()
            }
        }
    }
}

次に、「タッチイベントが競合してスクロールが効かない」を解決させるために上の方、または下の方に指が移動した場合に自動的にスクロールさせるために必要な設定を書いていきます。こちらの記事を参考に自動スクロールを実装しました。この辺はテキトーに改造したので、もっと効率の良い方があるような気がします。

ViewController.swift(160行目近辺-200行目近辺)
extension ViewController{
// 自動スクロールを開始する
    private func startAutoScroll(duration: TimeInterval, direction: String) {
        if isNeedToStartTimer {
            // 表示されているCollectionViewのOffsetを取得
            var currentOffsetY = collectionView.contentOffset.y
            // 自動スクロールを終了させるかどうか
            shouldFinish = false
            autoScrollTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: true, block: { [weak self] (_) in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    // item2つ分ずつスクロールさせる
                    switch direction {
                    case "up":
                        currentOffsetY = (currentOffsetY - 10 < 0) ? 0 : currentOffsetY - 10
                        self.shouldFinish = currentOffsetY == 0
                    case "down":
                        let highLimit = self.collectionView.contentSize.height - self.collectionView.bounds.size.height
                        currentOffsetY = (currentOffsetY + 10 > highLimit) ? highLimit : currentOffsetY + 10
                        self.shouldFinish = currentOffsetY == highLimit
                    default: break
                    }
                    UIView.animate(withDuration: duration, animations: {
                        self.collectionView.setContentOffset(CGPoint(x: 0, y: currentOffsetY), animated: false)
                    }, completion: { _ in
                        if self.shouldFinish { self.stopAutoScrollIfNeeded() }
                    })
                }
            })
        }
        isNeedToStartTimer = false
    }

    // 自動スクロールを停止する
    private func stopAutoScrollIfNeeded() {
        DispatchQueue.main.async {
            self.isNeedToStartTimer = true
            self.shouldFinish = true
            self.view.layer.removeAllAnimations()
            self.autoScrollTimer.invalidate()
        }
    }
}

最後に、ViewDidAppearで画面等のサイズを取得し、
スクロールを開始する高さの設定とセルサイズの設定をします。
scrollStartHeightTop変数とscrollStartHeightBottom変数がスクロールを開始する高さで、cellSize変数がセルの大きさの変数です。

ViewController.swift(45行目付近)
    override func viewDidAppear(_ animated: Bool) {
        scrollStartHeightTop = self.view.safeAreaInsets.top + 50
        scrollStartHeightBottom = scrollStartHeightTop + collectionView.frame.height - 100
        let width = ( self.view.frame.width - 9 * (numOfRow + 1) ) / numOfRow
        cellSize = CGSize(width: width, height: width)
        collectionView.reloadData()
    }
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?