質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.61%

【Swift】スクロールするたびにCollectionViewのセルの順序が変わる

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,602

score 38

メルカリのような通販アプリを作っていて、出品した商品を画像付きでタイムライン形式で表示する画面を作っています。

イメージ説明

別画面でFirebaseにデータを保存して、ホーム画面でデータを取得し、取得した順に表示したいのですが..。

今直面している問題は、スクロールして隠れたセルの画像(配置?)がなぜか入れ替わっていることです。

イメージ説明

データを格納する配列を出力しましたが、

  • スクロール中に同じデータを新たに取得してはいない
  • 配列内の順序が変わってはもいない
    という結果で原因がわかりません。

UICollectionViewの元々の仕様なのか、それとも単に隠れたセルは読み込む前の状態に戻ってしまうからなのか、何が問題なのでしょうか?

どうか、ご回答よろしくお願いします。

import UIKit
import Firebase
import FirebaseUI
import SDWebImage

class Home: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {

    @IBOutlet weak var collectionView: UICollectionView!

    //Firebaseから取得したItemIDを格納する
    var photos = [String]()
    //cellForItemAtが実行されたカウント
    var count_cellfunc = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.delegate = self
        collectionView.dataSource = self
        downloadImageData()
    }

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        count_cellfunc += 1
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        if count_cellfunc >= photos.count {
            return cell
        }
        //imageViewを宣言
        let imageView = cell.contentView.viewWithTag(1) as! UIImageView
        let storageref = Storage.storage().reference(forURL: "gs://bookshare-b78b4.appspot.com").child("userID").child("Item").child(photos[indexPath.row])
        imageView.sd_setImage(with: storageref)
        return cell
    }

    //FirebaseからItemのURLを取得する
    func downloadImageData() {
        let ref = Database.database().reference(fromURL: "https://bookshare-b78b4.firebaseio.com/")
        self.photos = [String]()
        ref.child("Item").observe(.value) { (snap) in
            for item in snap.children {
                let snapdata = item as! DataSnapshot
                //1つのデータ
                print(snapdata)
                let item = snapdata.value as! [[String:String]]
                self.photos.append(item[0]["ItemID"]!)
                print(self.photos)
            }
            self.collectionView.reloadData()
        }
    }

}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

ソース拝見したところ、UICollectionViewにおけるセルの再利用のされ方に、やや誤解があるように見えます。

cellForItem(at:)のメソッド内の処理について
dequeueReusableCell(withReuseIdentifier:for:)メソッドで生成(or再利用)されたセルを取得・返却していますが、再利用される際には同じindexPathのセルが再利用されるわけではありません。

例えば、一度下にスクロールしてセルが表示された後に、上にスクロールしなおした場合、画面外に出た下のセルが新しく上から表示されるセルに再利用されたりします。

提示していただいたソースの以下の箇所で、cellの中身を更新せずに再利用されたセルを返却しているので、count_cellfuncの値がphotos.countを上回った段階から再利用されたセルがそのまま表示されることになります。

※そのため、上にスクロースしたときに、画面外に出ていったものがまた上から出てきたり、順番がおかしくなったり、みたいな現象が発生してるものと思われます。

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    count_cellfunc += 1
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    /* ------ ここから ------ */
    if count_cellfunc >= photos.count {
        /* 中身を更新せずにcellを返却してしまっている。 */
        return cell
    }
    /* ------ ここまで ------ */
    /*中略*/
}

簡単な対応としては、上記のcellを返却しているif文の箇所をコメントアウトすれば、毎回cellの画像がindexPathに応じたものにセットし直されることになるので想定している挙動になるかと思います。


補足

let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)


上記の処理でCollectionViewから"cell"というIDの付いたセルを取得しています。
この時、CollectionViewは同じIDを持った再利用可能なセルが有ればそれを取得し、
再利用可能なものがなければ新しく生成したセルを返却してきます。

再利用可能かどうかは、基本的に画面に表示中かどうかで判断されているようなので、
画面外に出たセルが再利用される形になります。
※ちなみに、画面に表示中の有効なセルはCollectionViewのvisibleCellsという配列に格納されています。

前述にも軽く書きましたが、dequeueReusableCell(withReuseIdentifier:for:)メソッドの引数のIDが同じものの中でセルの再利用が行われます。
極端な話、すべてのセルで異なるIDを使えば再利用が行われず、indexPathでユニークにすることも可能です。。
※ただし、すべてのセルがメモリ上にキャッシュされることになってしまうので、推奨される方法ではありません。
(あと、事前にセルのIDを決めて準備しておかないといけないので、面倒です。)

作ったセルは再利用されるものと思って実装しておいたほうが、今回の様な現象を見た時に対処しやすいと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/02/05 17:00

    ご指摘のあったコードをコメントアウトしたら解決しました。

    1つ質問があるのですが、
    ・"return cell"は既に使用したセル(いずれかの画像がセットされた)を返却している
    という解釈でよろしいでしょうか?

    1度作られたセルは固定されず、取得・返却のプロセスを必ず踏むという理解で構いませんか?

    キャンセル

  • 2019/02/05 17:58

    return cellが必ずしも既に使用したセルを返却している、とは限らないですが元のソースではcount_cellfuncで判定を行っていたので、ほぼ再利用されたものが表示されていたと思います。

    ※セルの再利用については回答に補足を追記しましたのでそちらも参照ください。

    基本的には作られたセルは再利用される前提で考えておくのが良いと思います。

    キャンセル

  • 2019/02/06 12:32

    追加の質問にも答えていただきありがとうございます!!

    基礎的な内容なのに理解していなかった自分が情けないです..。

    しっかり覚えておきます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.61%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る