【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にカスタムセルを登録する際に
そもそも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)
}
このラベルにはセルごとに番号をふります。「セルごとに値が違う箇所」だけ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の話しないと思ったのか、さっぱりわからん。まあいいや。
全体コード
とりあえず目標物ができましたー!わーい
https://gist.github.com/uruly/8f9887297bc67cf800ef728cee9d1ddd
GitHubの方に載せてあるのは随時更新予定ですが、こちらはとりあえずバージョンとかが変わらない限りこのままで置いておきます。
UICollectionViewの可能性
自分はUIKitの中でUICollectionViewが一番好きです。というか一番使ってきたと思います。
Robinなんかは特に画像をコレクションするアプリなので、もう全ビューで使ってるんじゃないの?ってぐらい使ってます。
私の作ったAppAppというアプリでは、UICollectionViewをチュートリアルに使ったり、画像だけでないコレクションで使ったりしています。
チュートリアルはUIPageViewControllerを使う方がいいかもしれませんが、ページに規則性がある場合はコレクションビューでもありだと思います。
AppAppのコレクション画面では、カテゴリ分けをするラベルの部分もコレクションビューです。
同じコレクションビューで同じカスタムセルしか使えない!ってことはないので、実は結構なんでもありなんです。
例えばこの詳細画面も実はコレクションビューを使ってます。
今思うとここでコレクションビュー使うのは違うくない?wwって思うんですけど、どこで使っているかわかるでしょうか。
答えはこうです。
わりと頭おかしい実装な気もしますね。
ベース部分にコレクションビューを使い、そのセル上にテーブルビューを乗せています。
何故こういう実装にしたのかというのは、メモ部分にあります。可変セルをして、それに合わせてコンテンツサイズが変わる、というので使ってます。
でもこれ、UITableViewの入れ子でよかったのでは?という印象。っていうかそもそも入れ子にする必要もなかったのでは?www
記憶する限りUITableViewを使ってたけれど上手くいかなくて、当時から得意だったUICollectionViewを使った、という理由だった気がします。
実際には満足に動くものが作れているので、本当正解ってひとつじゃないです。
今回のカルーセルもUIScrollViewだけを使った方法もできるだろうし、UIPageViewControllerを使った方法でも作れると思います。
紹介した方法はあくまで1方法であります。
とりあえず今回のカルーセルシリーズはここで終わりにする予定です。
機会があれば、タイマーで自動的にスクロールするようにしたり、別のアニメーションをつけてみたり、そういった記事も書くかもしれません。確定ではないです。
長くなりましたが!この辺で!
Swift記事書くの楽しいね!でもしばらくSwiftにガッツリ向き合えないので、次回のSwift記事は結構先になるかもです。
ではでは〜。
UICollectionViewのカルーセルシリーズでした! 第1回:無限スクロール編 第2回:セルの拡大縮小編 第3回:ページング編 第4回:コレ
コメントはありません。
現在コメントフォームは工事中です。