【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【無限スクロール編】

どうも、Reoです。数ヶ月ぶりのSwift記事です!

今回はiOSで「カルーセル」を実装してみたので紹介していこうと思います。

IB/StoryBoardは使ってません。

 

完成予定

こんなのを実装していきます。

gifで見るとカクカクですが、動画で見るとヌルッとしててとても気持ち良いです。
ScreenRecording_06-11-2018 00-29-04
実はカルーセルって単語をしっかり覚えたのはつい最近だったり。それまでスライドパネルとか、なんかトップページによくあるスライドショーのやつって言ってました。

漫画アプリなんかでもよく見ますね!
マンガPark|人気の漫画が毎日更新のマンガアプリ – HAKUSENSHA.INC

私の1番好きなマンガアプリのトップページにも使われています。花とゆめ大好きなのでマンガParkは本当素晴らしい。まだ入れてない方は是非に(布教)

 

全部を一記事にすると結構長くなりそうなので分割して書いていこうと思います。全体はgithubにあげてあります。 uruly/CardCarousel: writting Swift4. iOS Carousel UI.

第1回:コレ
第2回:セルの拡大縮小編
第3回:ページング編
第4回:セルの装飾編

 

まず今回の記事では「無限スクロールするコレクションビュー」を実装していきます。

 

UICollectionViewのサブクラスを作ろう!

上部ナビゲーションよりFile > New > File … より CocoaTouchClassを選択し、UICollectionViewのサブクラスのCarouselViewを作成します。

 

CarouselView.swiftができたら、まずはイニシャライザを書いておきます。

import UIKit

class CarouselView: UICollectionView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
    }
    
}

collectionViewLayoutを固定にして、インスタンス生成時にはframeだけを呼びたいので、convenience initを書いておきます。

class CarouselView: UICollectionView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
    }
    
    convenience init(frame: CGRect) {
        let layout = UICollectionViewFlowLayout()
        
        self.init(frame: frame, collectionViewLayout: layout)
    }
    
}

 

UICollectionViewCellのサブクラスを作ろう!

次にCarouselViewで使うセルをカスタマイズするために、UICollectionViewCellのサブクラスを作ります。

上部ナビゲーションよりFile > New > File … より CocoaTouchClassを選択し、UICollectionViewCellのサブクラスのCarouselCellを作成します。

中身は後々カスタマイズするので、今はとりあえずファイルができて入ればおkです。

 

CarouselViewを配置しよう!

UICollectionViewを使う際の最低限の設定を書いていきます。

まずはdelegateを書きます。

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        self.delegate = self
        self.dataSource = self
    }

先ほど作ったセルを登録します。

    let cellIdentifier = "carousel"
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        self.delegate = self
        self.dataSource = self
        self.register(CarouselCell.self, forCellWithReuseIdentifier: cellIdentifier)
    }

これだけだとまだdelegateとdataSourceのところにエラーが出ていますが、extensionを書けば消えるので放置しておきます。

extensionでUICollectionViewDelegateとUICollectionViewDataSourceプロトコルに適応するようにします。

extension CarouselView: UICollectionViewDelegate {
    
}

extension CarouselView: UICollectionViewDataSource {
    
}

ほいでDataSourceの方にセル数のセルの中身を決める関数が必須なので書いておきます。

extension CarouselView: UICollectionViewDataSource {
    
    // セクションごとのセル数
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.pageCount
    }
    
    // セルの設定
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        
        return cell
    }
    
}

CarouselViewクラス内で適当にセルの数をpageCountという定数で宣言してます。

ここまでで配置ができるようになったので適当に配置したい場所(今回はViewController)に配置します。

import UIKit

class ViewController: UIViewController {
    
    var carouselView:CarouselView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let width = self.view.frame.width
        let height = self.view.frame.height
        
        carouselView = CarouselView(frame: CGRect(x:0, y:0, width:width, height:height))
        carouselView.center = CGPoint(x:width / 2,y: height / 2)
        self.view.addSubview(carouselView)
    }
    
}

これで実行できるようになりますが、まだ真っ黒です。

とりあえず表示させるためにセルの大きさを決めて、セルに色をつけて置きます。ついでに背景色を白にしておきました。

    convenience init(frame: CGRect) {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 200, height: frame.height / 2)

        self.init(frame: frame, collectionViewLayout: layout)

        self.backgroundColor = UIColor.white
    }

 

    // セルの設定
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        cell.contentView.backgroundColor = UIColor.green
        return cell
    }

これでとりあえず表示されるようになりました。

ここまではUICollectionViewのサブクラスを作るときに必ずやっています。多分もうgistにでもテンプレートとしてあげておいたほうが楽な気もします。(いつも歌いながら手打ちしてる)

 

スクロールの方向を横向きにする

とりあえずスクロールの向きを水平にしておきます。

    convenience init(frame: CGRect) {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 200, height: frame.height / 2)
        layout.scrollDirection = .horizontal
        
        self.init(frame: frame, collectionViewLayout: layout)
        
        self.backgroundColor = UIColor.white
    }

 

超簡単!一行で出来ます。

スクロールバーが邪魔なので消して置きます。

    convenience init(frame: CGRect) {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 200, height: frame.height / 2)
        layout.scrollDirection = .horizontal
        
        self.init(frame: frame, collectionViewLayout: layout)
        
        // 水平方向のスクロールバーを非表示にする
        self.showsHorizontalScrollIndicator = false
        self.backgroundColor = UIColor.white
    }

ここまでで実行するとこんな感じです。

 

これで横向きのコレクションビューができました。∩(〃・ω・〃)∩

ここまでのコード(横方向コレクションビュー)

無限スクロールをする前に、ここまでの全体コードです。一応gistにあげました。

ここまでで既に長い。

 

色がわかりやすいようにrowごとにセルの色を変えておきます。

class CarouselView: UICollectionView {
    let colors:[UIColor] = [.blue,.yellow,.red,.green,.gray]
    //略
}

 

    // セルの設定
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
        cell.contentView.backgroundColor = colors[indexPath.row]
        
        return cell
    }

 

無限スクロールをしよう!

いよいよ今回の記事の本題です。CollectionViewを無限スクロールできるようにします。

無限スクロールのオンオフはBool値で簡単に制御できるようにしておきます。

class CarouselView: UICollectionView {
    let isInfinity = true
    //略
}

 

まず、実際のセル数の3倍のセルを用意します。

    // セクションごとのセル数
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return isInfinity ? pageCount * 3 : pageCount
    }

ほいで、実際に表示したいセル達のWidthをいれておく変数を宣言しておきます。

class CarouselView: UICollectionView {
    var cellItemsWidth: CGFloat = 0.0
    // 略
}

 

UICollectionViewは、UIScrollViewDelegateプロトコルに適合しているので、それを利用して、コレクションビューがスクロールされているときに呼び出される関数内に以下のように書きます。

extension CarouselView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        if isInfinity {
            if cellItemsWidth == 0.0 {
                cellItemsWidth = floor(scrollView.contentSize.width / 3.0) // 表示したい要素群のwidthを計算
            }
            
            if (scrollView.contentOffset.x <= 0.0) || (scrollView.contentOffset.x > cellItemsWidth * 2.0) { // スクロールした位置がしきい値を超えたら中央に戻す
                scrollView.contentOffset.x = cellItemsWidth
            }
        }
    }
    
}

3倍表示してるので3で割ってごにょごにょ。

この辺はだいぶ昔に以下の記事を参考にさせていただいて作っています。

UIPageViewControllerをつかって無限スクロールできるタブUIを実装してOSSとして公開しました – Start Today Technologies TECH BLOG

 

最後に、実際に表示させるセルの中身をindexを修正して表示させます。

    // セルの設定
    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]
    }

configureCellの方でセルの設定をするようにしてます。

実際のindexPath.rowを使うと要素の3倍数までが配列に入ることになってしまうので修正しておきます。

 

これで実行!

 

無限スクロールできました!yeah!

 

全体コード

これもさっきのとは別物としてgistにあげました。

次回、セルの大きさを変更する!

とりあえず今回はこの辺で。

次回は完成品のように、真ん中にくるセルと左右のセルの大きさを変える処理の紹介をしていこうと思います。

物自体はできているのであとはブログを書くだけなのですが、とりあえず長くなったので一度休憩します。

 

githubに完成品のコードをあげているので、そちらも参照してください。

uruly/CardCarousel: writting Swift4. iOS Carousel UI.

 

次記事書き次第リンク貼っておきます。
次 > 【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【セルの拡大縮小編】

 

無限スクロール自体はわりと何度か作っているので、もうほとんど以前のプロジェクトを見ればできちゃうんですが、昔は結構ハマってた気がします。

作ってるわりには全然自分のアプリでは無限スクロール使ってないですね。

実は無限スクロールだけでなく、extensionでの書き方を学んだのも先ほど貼った記事でした!自分だけで50アクセスぐらいしてそう。いや、もっとしてるかもしれないです。

以下にも貼っておきますので是非是非!

それではまた次回〜

第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