[iOS] 左右のマージン部分で前後のバナーが少し見えているカルーセルUIを作りたい

2021.07.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。きんくまです。

今回は前後が少し見えているバナー(カルーセルUI)を作りたい。です。

つくったもの

仕様

  • 横スワイプでページをきりかえできる
  • バナーの左右に、前のバナーと後ろのバナーが少し見える
  • 画面下のページコントロールがバナーの位置に連動
  • ページコントロールでもバナーをきりかえ
  • 端末のサイズに応じて、比率で幅や高さを適用させる
  • バナーをタップするイベントを取得して、どのバナーがタップされたかわかる
  • バナーをスクロールして先頭に戻すことが可能

ソースコード

GitHubにアップしました
cm-tsmaeda / BannerViewSample

カスタマイズ

BannerScrollViewControllerのこのあたりは、デザイン上の値を設定します。
そうすると、その値と端末の横幅から比率を計算してレイアウトします。

    /// バナーエリアの全体の幅
    let baseComponentWidth: CGFloat = 406
    /// デザインでのパネルの幅
    let basePanelWidth: CGFloat = 355
    /// デザインでのパネルの高さ
    let basePanelImageHeight: CGFloat = 238
    /// デザインでのテキスト部分の高さ
    let basePanelTextContainerHeight: CGFloat = 30
    /// デザインでのパネルの横マージン(先頭の左マージン)
    lazy var basePanelHorizontalMargin: CGFloat = (baseComponentWidth - basePanelWidth) / 2
    /// デザインでのパネル間のマージン
    let basePanelGap: CGFloat = 8

実装のポイント

ページング

UIScrollViewの isPagingEnabled をtrueにすると、ページングが可能になります。
ただし、ScrollViewのframeに合わせて表示されるので、前後はそのままだと表示されません。
なので、 clipsToBounds をfalseにすることで、見えていなかった部分が見えるようになります。

しかしここで問題がおきます。
frameの外にある部分は当たり判定(hitTest)が機能しないのです。
で、それを解決しているのが以下の部分です。

BannerScrollViewController.swift

        // frameの外はタッチイベントがきかない。その回避策
        // https://stackoverflow.com/a/36641652
        view.addGestureRecognizer(scrollView.panGestureRecognizer)

現在のページindexをもとめる

ScrollViewのcontentOffsetに応じて、現在のページindexを計算します。indexが変わったら自作のdelegateにイベントを発行します。

BannerScrollViewController.swift

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let currentIndex = pageIndex
        let newIndex = calcPageIndex(contentOffsetX: scrollView.contentOffset.x)
        if currentIndex != newIndex {
            pageIndex = newIndex
            delegate?.bannerScrollViewController(self, didChangePageIndex: newIndex)
        }
    }
    
    /// 現在のページindexを返す
    func calcPageIndex(contentOffsetX: CGFloat) -> Int {
        // 0.5 - 1.5 の間は1で返す
        // 1.5 - 2.5 の間は2で返す....
        return Int(round(contentOffsetX / (panelWidth + panelGap)))
    }

UIPageControlとの連動

  • UIScrollViewでページがきりかわったらUIPageControlをきりかえる
  • UIPageControlの値がかわったら、UIScrollViewのページをきりかえる

ということをやります。

ViewController.swift

    func updateBannerPageControl() {
        bannerPageControl.currentPage = bannerViewController.pageIndex
    }
    
    @IBAction func didChangeBannerPageControl() {
        bannerViewController.showPage(index: bannerPageControl.currentPage, animated: true)
    }
    
    @IBAction func didTapResetButton() {
        bannerViewController.showPage(index: 0, animated: true)
    }
    
    func bannerScrollViewController(_ scrollViewController: BannerScrollViewController, didChangePageIndex index: Int) {
        updateBannerPageControl()
    }

まとめ

バナーつくってみました。ではでは。

余談

実は一番ひっかかったところは、xibファイルとUIViewのクラスファイルのひもづけですw
(今回のメイン部分とは全く無関係)

  • BannerView.xib
  • BannerView.swift(UIViewのサブクラスで、BannerView.xibのClass設定で使う)
  • BannerViewController.swift(BannerView.xibのClass設定では使わない)

という3つのファイルがあって、BannerViewをUINibからinitしたら実行時エラーがおきてクラッシュしまくり。
なんでかわからず、とほうにくれていたら、BannerViewをinitするときに、同じ名前のViewControllerがあると暗黙的にそっちにリンクすることを知りました。
そんなことを知らんがなーw

なので、BannerViewController.swiftの名前をBannerScrollViewControllerに変更しました。
気をつけよう!命名規則!!