【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【セルの装飾編】

どうも。Reoです。

UICollectionViewを使ってカルーセルを実装しようシリーズの第4回目です。

第1回:無限スクロール編
第2回:セルの拡大縮小編
第3回:ページング編
第4回:コレ

 

目標物はコレ!

 

第1回から第3回までで、動きの実装はすべて終わりました。

一応目標物の形になるまでの実装を紹介するという程で記事を書き始めたので、今回は「セルの装飾」をしていこうと思います。

ここまできたらオマケみたいなもんですが、せっかくなので!(実のところ、ここのコードが一番同業者に見せるのが怖いと思っていますw

 

それでは完成物と同じになるようにセルの装飾をしていきます!

プロジェクトはこれまで使用してきたものを使います。(ここまでのコード

 

今回使用するファイルはUICollectionViewCellのサブクラス「CarouselCell」です。

私はIB/StoryBoardを使わない派閥の人間なので、今回はこのファイルにコードを書いていきます。

イニシャライザを書こう!

下準備です。CarouselCellにイニシャライザを書きます。

import UIKit

class CarouselCell: UICollectionViewCell {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setup()
    }
    
    func setup() {
        // ここでセットアップする
    }

}

こんな感じに。setupを呼び出すために書いています。

xibベースの場合のイニシャライザはawakeFromNibを使いますが、コードベースの場合だとinit(frame:)を使います。

具体的にはUICollectionViewにカスタムセルを登録する際に


上記のどちらを使うかで必要なイニシャライザが変わります。

cellClassの場合はinit(frame:)、nibの場合はawakeFromNibです。

 

そもそもxibを使ってたらあれかな、IB上で設定できるんかな…(やったことないから知らない)

一時期xibにチャレンジしたときにここで詰まったことがあります。

ちなみにUITableViewCellのときも同様です。

 

セルにラベルを設置しよう!

まずは、セルにラベルを設置します。完成物の数字ラベルの部分です。

var countLabel:UILabel!
    
func setup() {
    // セルの縦横を取得する
    let width:CGFloat = self.contentView.frame.width
    let height:CGFloat = self.contentView.frame.height
    
    // 適当なmargin
    let margin:CGFloat = 15
    
    // 数字ラベルを設置する
    countLabel = UILabel()
    countLabel.frame = CGRect(x:margin,
                              y:margin,
                              width:width - margin * 2,
                              height:50)
    countLabel.textAlignment = .center
    countLabel.textColor = UIColor.black
    countLabel.font = UIFont.systemFont(ofSize: 22, weight: .bold)
    
    self.contentView.addSubview(countLabel)
}

addSubviewをするときはcontentViewの方にしてください。cellに直接addSubviewをするとうまく動かないことがあります。

 

このラベルにはセルごとに番号をふります。「セルごとに値が違う箇所」だけCollectionViewのcollectionView(_:cellForItemAt:)にて値を設定します。

extension CarouselView: UICollectionViewDataSource {
    
    //略
    
    // セルの設定
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell:CarouselCell = dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CarouselCell
        
        configureCell(cell: cell, indexPath: indexPath)
        
        return cell
    }
    
    func configureCell(cell: CarouselCell,indexPath: IndexPath) {
        // indexを修正する
        let fixedIndex = isInfinity ? indexPath.row % pageCount : indexPath.row
        cell.contentView.backgroundColor = colors[fixedIndex]
        
        // countLabelにIndexをつける
        cell.countLabel.text = String(fixedIndex + 1)
    }
    
}

今回はconfigureCellの方に分離して書いています。前回コードから付け足したのは19,20行目だけです。

実はコードでAutoLayoutを使ったことがほぼ無い人間なので、毎回座標で設置してます。あんまり困ったことはないです。むしろAutoLayout使うときはIBを使ってます。あんまり機会はないけど。

 

見えづらいですがラベルが設置されました。

 

self.frameではなくself.contentView.frameなワケ。

少し前にこのブログを改装した際に、全てのSwift記事をチェックしました。

そのときに以下2つの記事

【Swift】UICollectionViewでチェックマークをつける

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

のチェックをしている際に、ラベルのframeにセルのframe(self.frame)を直接設定すると、セルが二重に表示されてしまうということがありました。

その時に使ったコードはこちら→CheckCollectionView.swift – gist

 

せっかくなので少しだけ検証しました。上記のCheckCollectionView.swiftを用います。

 

正しい表示はコレ

正しい表示

間違えるとこんな感じに。

カスタムセルのコードは

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.textAlignment = .center
        self.contentView.addSubview(label)
    }
    
}

とこんな感じ。変更するのはハイライトした16行目の部分です。

 

self.contentView.frameの場合

これは正しく動作します。自分はいつもコレを設定してます。(多分)

func setup() { //正しく動作する
    label = UILabel(frame:self.contentView.frame)
    label.textAlignment = .center
    self.contentView.addSubview(label)
}

self.frameの場合

これはダメです。バグります。バグらない場合もありますがイクナイです。

func setup() { //二重に表示されてしまう。
    label = UILabel(frame:self.frame)
    label.textAlignment = .center
    self.contentView.addSubview(label)
}

 

self.boundsの場合

これはオッケーです。

func setup() { //正しく動作する
    label = UILabel(frame:self.bounds)
    label.textAlignment = .center
    self.contentView.addSubview(label)
}

self.contentView.boundsの場合

これも問題ないです。むしろコレを設定するのが本来一番いいのかな。どうだろう。

func setup() { //正しく動作する
    label = UILabel(frame:self.contentView.bounds)
    label.textAlignment = .center
    self.contentView.addSubview(label)
}

 

イニシャライザのframeの場合

こういうの

override init(frame: CGRect) {
    super.init(frame: frame)
    //xibを使わない場合はこちらでセットアップ
    setup(frame: frame)
}

func setup(frame: CGRect) {
    label = UILabel(frame:frame) // 二重に表示される
    label.textAlignment = .center
    self.contentView.addSubview(label)
}

コレもダメでした。いけそうなのにね。

 

frameはsuperviewを基点とした座標で、boundsは自分自身のローカル座標になります。

なのでうまくいかない理由はなんとなく分かります。

細かく見るとこういうことです。

// これは上手くいく
label.frame.origin = self.bounds.origin
label.frame.size = self.frame.size

// コレはダメ
label.frame.origin = self.frame.origin
label.frame.size = self.frame.size

座標の方がズレるので二重に表示されてしまったりバグってしまうワケですね。

セルが回転したりするとself.frame.sizeの方も変わってくることになるので、その辺も気をつける必要はあります。

今回はとりあえずこれ以上は言及しません。ググればframeとboundsの話はいっぱい出てくるので。

 

とにかくセル上にaddSubviewしたラベル等がうまくいかないときは、こういうケースもあったよ!っていう参考になれば嬉しいです。

 

セルに枠線をつける

だいぶ脱線してしまいました。カルーセルの方に戻ります。

セルの背景色を白、これまで背景色になっていた色を枠線の色に変えていきます。

class CarouselCell: UICollectionViewCell {
    
    // 略
    
    func setup() {
        
        // 略
        
        // セルの背景色を変える
        self.contentView.backgroundColor = UIColor.white
        
        // セルの枠線の太さを変える
        self.contentView.layer.borderWidth = 2
    }
}

ConfigureCellの方で枠線の色をつけます。

func configureCell(cell: CarouselCell,indexPath: IndexPath) {
    // indexを修正する
    let fixedIndex = isInfinity ? indexPath.row % pageCount : indexPath.row
    
    //cell.contentView.backgroundColor = colors[fixedIndex]
    cell.contentView.layer.borderColor = colors[fixedIndex].cgColor
    
    // countLabelにIndexをつける
    cell.countLabel.text = String(fixedIndex + 1)
}

 

backgroundColorに色をつけていたのは削除しておきます。

こんな感じに。

セルを角丸にしよう!

簡単一行!

func setup() {
    
    // 略
    
    // セルを角丸にする
    self.contentView.layer.cornerRadius = 10
}

まるっと

セルに影をつけよう

最後!セルに影をつけます。

func setup() {

    // 略
    
    self.contentView.layer.shadowOffset = CGSize(width: 1,height: 1) // 影の位置
    self.contentView.layer.shadowColor = UIColor.gray.cgColor        // 影の色
    self.contentView.layer.shadowOpacity = 0.7                       // 影の透明度
    self.contentView.layer.shadowRadius = 5                          // 影の広がり
    
}

 

これで完成形になりました!ワーイ

それぞれのプロパティですが、

shadowColorは影の色

shadowOpacityは影の透明度

というのはもうそのままなので説明しようがないです。

 

影の基本的な表示

基本的に影はshadowOpacityを0より大きくすることで発動します。

 

影は例えば背景色が透過されたセルがあった場合。

// セルの背景色を透明にする
self.contentView.backgroundColor = UIColor.clear
        
// セルは枠線だけ
self.contentView.layer.borderWidth = 2
        
// セルに影をつける
self.contentView.layer.shadowOffset = CGSize(width: 20,height: 20) // 影の位置
self.contentView.layer.shadowOpacity = 1                       // 影の透明度

 

これを実行すると

こんな表示になります。

透過されている部分は影にはならず、枠だけに影がつきます。

 

このセルに背景色をつける(clear以外にする)と

// セルの背景色を白にする
self.contentView.backgroundColor = UIColor.white
        
// セルは枠線だけ
self.contentView.layer.borderWidth = 2
        
// セルに影をつける
self.contentView.layer.shadowOffset = CGSize(width: 20,height: 20) // 影の位置
self.contentView.layer.shadowOpacity = 1                       // 影の透明度

色のついている部分の影ができます。

 

shadowOffsetはどれだけズラすか

shadowOffsetは設定しないと(0,-3)になるっぽいです。

shadowOffsetが(0,0)の場合、セルの真下にセルと同じサイズの影ができていることになります。

ビューと同じ座標から、どれだけズラすかを設定します。これSizeよりPointだと思う方がわかりやすい気もします。(なんでサイズなんだろう・・・)

 

(0,0)にすると全く見えないわけではありません!

shadowRadiusで影の広がりを変える

shadowOffsetが(0,0)でもshadowRadiusが0より大きければ影は表示されます。

// セルに影をつける
self.contentView.layer.shadowOffset = CGSize(width: 0,height: 0) // 影の位置
self.contentView.layer.shadowOpacity = 1                       // 影の透明度
self.contentView.layer.shadowRadius = 10                         // 影の広がり

 

反対に、shadowRadiusが0でもshadowOffsetがずれていれば影は表示されます。

// セルに影をつける
self.contentView.layer.shadowOffset = CGSize(width: 10,height: 10) // 影の位置
self.contentView.layer.shadowOpacity = 1                       // 影の透明度
self.contentView.layer.shadowRadius = 0                         // 影の広がり

見てわかる通り影のぼやけ具合が違いますよね。

 

イメージは、ビューと同じサイズの真っ黒の影をブラシで指定したピクセル分ぼかす感じです。
フォトショとかでブラシ使ってぼやかすみたいな。

 

masksToBoundsの設定

影の話をすると多分必ずついてくると言っても過言ではないのがマスクの話です。(今回は使ってないので過言かもしれないですが)

// セルの背景色を変える
self.contentView.backgroundColor = UIColor.white

// セルに影をつける
self.contentView.layer.shadowOffset = CGSize(width: 1,height: 1) // 影の位置
self.contentView.layer.shadowOpacity = 1                       // 影の透明度
self.contentView.layer.shadowRadius = 10                         // 影の広がり

// contentViewからはみ出た部分を表示しない
self.contentView.layer.masksToBounds = true

こうすると、セルからはみ出た部分はマスクされて表示されません。

例えば背景色をclearにすると内側に影を落とすことができます。

なんかしたかった話と違う気がする。忘れた・・・。

内側に影を落とすのはaddSubviewしたやつも影ついちゃうのでshadowPathを使うとか別のやり方のがいいと思います。

 

忘れた。なぜ影の話ししたからmasksToBoundsの話しないと思ったのか、さっぱりわからん。まあいいや。

 

全体コード

とりあえず目標物ができましたー!わーい

GitHubの方に載せてあるのは随時更新予定ですが、こちらはとりあえずバージョンとかが変わらない限りこのままで置いておきます。

 

UICollectionViewの可能性

自分はUIKitの中でUICollectionViewが一番好きです。というか一番使ってきたと思います。

Robinなんかは特に画像をコレクションするアプリなので、もう全ビューで使ってるんじゃないの?ってぐらい使ってます。

 

私の作ったAppAppというアプリでは、UICollectionViewをチュートリアルに使ったり、画像だけでないコレクションで使ったりしています。

AppAppのチュートリアル

 

 

AppAppのコレクション画面

 

チュートリアルはUIPageViewControllerを使う方がいいかもしれませんが、ページに規則性がある場合はコレクションビューでもありだと思います。

AppAppのコレクション画面では、カテゴリ分けをするラベルの部分もコレクションビューです。

 

同じコレクションビューで同じカスタムセルしか使えない!ってことはないので、実は結構なんでもありなんです。

AppAppの詳細画面

例えばこの詳細画面も実はコレクションビューを使ってます。

今思うとここでコレクションビュー使うのは違うくない?wwって思うんですけど、どこで使っているかわかるでしょうか。

 

 

答えはこうです。

1つのコンテンツを1つのセルに

わりと頭おかしい実装な気もしますね。

ベース部分にコレクションビューを使い、そのセル上にテーブルビューを乗せています。

何故こういう実装にしたのかというのは、メモ部分にあります。可変セルをして、それに合わせてコンテンツサイズが変わる、というので使ってます。

でもこれ、UITableViewの入れ子でよかったのでは?という印象。っていうかそもそも入れ子にする必要もなかったのでは?www

 

記憶する限りUITableViewを使ってたけれど上手くいかなくて、当時から得意だったUICollectionViewを使った、という理由だった気がします。

実際には満足に動くものが作れているので、本当正解ってひとつじゃないです。

 

今回のカルーセルもUIScrollViewだけを使った方法もできるだろうし、UIPageViewControllerを使った方法でも作れると思います。

紹介した方法はあくまで1方法であります。

 

とりあえず今回のカルーセルシリーズはここで終わりにする予定です。

機会があれば、タイマーで自動的にスクロールするようにしたり、別のアニメーションをつけてみたり、そういった記事も書くかもしれません。確定ではないです。

長くなりましたが!この辺で!

 

Swift記事書くの楽しいね!でもしばらくSwiftにガッツリ向き合えないので、次回のSwift記事は結構先になるかもです。

ではでは〜。

 

UICollectionViewのカルーセルシリーズでした!
第1回:無限スクロール編
第2回:セルの拡大縮小編
第3回:ページング編
第4回:コレ

Comments...

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

Write a Comment

コメント時の注意

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

Related Memo...

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

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

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

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

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

iOS

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

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

The inserted or deleted rows use the default animations.

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

 

iOS

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

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS
more