【Swift】UICollectionViewで二重に表示されるのを防ぐ

UICollectionViewを用いて画像を表示させていたのですが、スクロールすると以前の画像と重なって表示されるという事態に陥りました。

少しだけハマったけれどあまり時間もかからずに解決したのでよかったです。

 

原因としてはセルを再利用するときに再利用前の画像がそのまま表示されてしまっている、ということ。

 

addSubviewをするとデータが蓄積される

まず、collectionView:cellForItemAtIndexPath:のメソッド内でセルをカスタマイズします。セルを再利用するよ〜と宣言しておきます。

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
     let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell",forIndexPath:indexPath)
      //ここでセルをカスタマイズ
      return cell
}

 

そもそもここでセルをカスタマイズするのがあまりよろしくないらしいが、とりあえずそれは置いといて。

このセルの上にImageViewを配置してそこに画像を表示させます。

let imageView = UIImageView()
let img = UIImage(named:"image.jpg")! as UIImage
imageView.frame = CGRectMake(0,0,100,100)
imageView.image = img
cell.contentView.addSubview(imageView)

こんな感じ。

addSubviewをするときにcellに直接addSubviewもできますが、contentViewを挟んだほうが良いらしい。

そしてこのaddSubviewしたものはどんどん蓄積されていきます。二重に表示されるどころか、メモリ効率も悪くなってしまいます。

 

解決方法

addSubviewをする前にcellのsubviewを削除しておきます。

for subview in cell.contentView.subviews{
      subview.removeFromSuperview()
}

これをaddSubviewの前に書いておきます。そうすることでsubviewの中身を初期化しておくことができます。

 

ラベルとかも色々そこで書いていたのでとにかくカオスな状況になっていたのが解決してよかったです。

メソッド内のコードはこんな感じ。

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
     let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell",forIndexPath:indexPath)
     for subview in cell.contentView.subviews{
           subview.removeFromSuperview()
     }
      //ここでセルをカスタマイズ
     let imageView = UIImageView()
     let img = UIImage(named:"image.jpg")! as UIImage
     imageView.frame = CGRectMake(0,0,100,100)
     imageView.image = img
     cell.contentView.addSubview(imageView)
     return cell
}

 

参考にしているのがSwiftでもなければUICollectionViewでもないあたりがオモシロイ。笑

あとはデータの削除が終わればUICollectionViewに関してはほぼ完成です。頑張っていこー(´・ω・`)

2018/04/29追記 UICollectionViewCellのサブクラスを作ろう!
ついでにSwift4にも対応。

基本的にcollectionView(_:cellForItemAt:)内ではcellにaddSubviewしません。

UICollectionViewのサブクラスを適当に作って(CustomCellとしました)
import UIKit

class CustomCell: UICollectionViewCell {
    
    var label:UILabel!

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        //xibを使わない場合はこちらでセットアップ
        setup()
    }
    
    func setup() {
        label = UILabel(frame:self.contentView.frame)
        label.text = "らべる"
        self.contentView.addSubview(label)
    }
    
}
自分はxibを滅多に使わないのでこういう書き方してます。 んでこれをCollectionViewに登録
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
そしたらあとはcollectionView(_:cellForItemAt:)内で
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell:CustomCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CustomCell
        cell.label.text = "テキストだけを変える"
        
        return cell
    }
とすれば二重には表示されないはずです。 二重に表示される主な原因は、
  1. collectionView(_:cellForItemAt:)内でcellに直接addSubviewしている
  2. collectionView(_:cellForItemAt:)内で非同期処理をしている場合
の2点だと思います。 2番目は本当によく使うんですが、 cellが再利用された時点では非同期の読み込みが終わってなく、前のセルの情報が残っちゃうんですね。 なので
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell:CustomCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CustomCell
        //二重に表示されないように再利用時にimageView.imageを消しておく
        cell.imageView.image = nil
        
        //非同期で画像イメージを読み込む
        DispatchQueue.global().async {
            //適当に読み込んだ image があるとする
            DispatchQueue.main.async {
                //データをセル内にのせる
                cell.imageView.image = image
            }
        }
        
        return cell
    }
みたいにしておく必要があります。 と思ったけどこれ、二重には表示されない気がする。 でも何かしら対策はしてないと画像とテキストがあってないって事態になりがちです。 とりあえずcollectionView(_:cellForItemAt:)内ではaddSubviewしないようにしよう!ってことでした。 どうしてもaddSubviewしないといけないんじゃ・・・って人は本記事のようにremove処理しておけば良いハズ。 余談ですが、このUICollectionViewCellは何個でもCollectionViewにregisterで登録できるんですね。 なので1行目はこのレイアウトだけどそれ以降は別のレイアウトにしたい!って時はUICollectionViewCellのサブクラスを複数用意して、そちらを場合分けして使うのが良さげです。 ちょっと今次の記事の追記を行なっていてまさに二重になる現象に出くわしました!!!!!!!! カスタムセルで
    func setup() {
        label = UILabel(frame:self.frame)
        label.text = "らべる"
        self.contentView.addSubview(label)
    }
としてaddSubviewしていたらなんと再現してしまったwwww えっちゃんとカスタムセル作ってるし、今もうチェックマークつける話ししてるんだけど・・・って気持ちで笑ってしまいました。 解決策は
    func setup() {
        label = UILabel(frame:self.contentView.frame)
        label.text = "らべる"
        self.contentView.addSubview(label)
    }
labelのframeにself.frameを設定していると二重に表示されました。 これをself.contentView.frameにすると直りました。 フィィ

Comments...

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS
more