collection viewでカルーセルを実装してみた経験より(2020-07-27)

畑田です。
OTOnectの開発においてモード選択画面をカルーセルにしたくてcollection viewを使いました。全てコードで書いています。

環境

Swift version 5.2.4
Xcode version 11.6

collection viewを用いてカルーセルを実装

理想としてはカルーセルの隣のcellがチラッと見えていて、page controlも下についているというものです。
困ったことは、collection viewはcellのサイズが画面の幅と異なる場合、isPagingEnabled = trueにすると、cellの中心と画面の中心が合わずにページング(カチッとスクロールが止まるやつ)されてしまうという点です。
これでは嫌なので、画面と同じ大きさのcellを作って、(cellはcontentViewというデフォルトで透明なUIViewインスタンスを持つので)その上に本来cellとして扱いたかった内容を新しいviewとしてaddSubviewすることによって解決しました。ただしこれでは、cell自体の大きさは画面幅いっぱいなので、隣のcellをチラ見せできないという問題点を持っています。
ま、要は実装し直しです。とりあえずはこれでできたフリをしますが。https://techlife.cookpad.com/entry/2019/08/16/090000 これとか使えそう。
以下ソースコード。実装し直しですけど。

import UIKit

class CarouselCell: UICollectionViewCell {

    var cardView: UIView!

    var modeTitle: UILabel!
    var modeDescription: UILabel!

    var modeTitleText: String!
    var modeDescriptionText: String!

    var circleOutside: UIView!
    var circleMiddle: UIView!
    var circleInside: UIView!

    let paddingSize: CGFloat = 8

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

    override init(frame: CGRect) {
        super.init(frame: frame)

    }

    func setupUI() {

        setupCardView()
        self.contentView.addSubview(cardView)

        setupCircles()
        self.contentView.addSubview(circleOutside)
        self.contentView.addSubview(circleMiddle)
        self.contentView.addSubview(circleInside)

        setupModeTitle()
        self.contentView.addSubview(modeTitle)

        setupModeDescription()
        self.contentView.addSubview(modeDescription)
    }

    func setupCardView() {
        cardView = UIView()
        cardView.frame.size = CGSize(width: self.contentView.frame.width - 72 , height: 480)
        cardView.center = CGPoint(x: self.contentView.center.x, y: self.contentView.frame.height / 2)
        cardView.backgroundColor = .white
        cardView.layer.cornerRadius = 48

        cardView.layer.shadowColor = UIColor.black.cgColor
        cardView.layer.shadowOffset = CGSize(width: 12, height: 12)
        cardView.layer.shadowRadius = 48
        cardView.layer.shadowOpacity = 0.5
    }

    func setupCircles() {
        let circleCenter = CGPoint(x: cardView.center.x, y: cardView.center.y - 48)
        let circleRadius: CGFloat = 224
        let circleRadiusDiff: CGFloat = 48

        circleOutside = UIView()
        circleOutside.frame.size = CGSize(width: circleRadius, height: circleRadius)
        circleOutside.center = circleCenter
        circleOutside.backgroundColor = .red
        circleOutside.tintColor = .systemRed
        circleOutside.layer.cornerRadius = circleOutside.frame.size.width / 2

        circleMiddle = UIView()
        circleMiddle.frame.size = CGSize(width: circleRadius - circleRadiusDiff, height: circleRadius - circleRadiusDiff)
        circleMiddle.center = circleCenter
        circleMiddle.backgroundColor = .green
        circleMiddle.layer.cornerRadius = circleMiddle.frame.size.width / 2

        circleInside = UIView()
        circleInside.frame.size = CGSize(width: circleRadius - circleRadiusDiff * 2, height: circleRadius - circleRadiusDiff * 2)
        circleInside.center = circleCenter
        circleInside.backgroundColor = .blue
        circleInside.layer.cornerRadius = circleInside.frame.size.width / 2
    }

    func setupModeTitle() {
        let attributedModeTitleText = NSAttributedString(string: modeTitleText, attributes: [
            .foregroundColor: UIColor.black, .font: UIFont.boldSystemFont(ofSize: 28)])

        modeTitle = UILabel()
        modeTitle.frame.size = CGSize(width: 200, height: 48)
        modeTitle.center = CGPoint(x: cardView.center.x, y: cardView.center.y + 108)
        modeTitle.attributedText = attributedModeTitleText
        modeTitle.textAlignment = .center
    }

    func setupModeDescription() {
        let attributedModeDescriptionText = NSAttributedString(string: modeDescriptionText, attributes: [
        .foregroundColor: UIColor.gray, .font: UIFont.systemFont(ofSize: 16)])

        modeDescription = UILabel()
        modeDescription.frame.size = CGSize(width: 200, height: 48)
        modeDescription.center = CGPoint(x: cardView.center.x, y: modeTitle.frame.maxY + 24)
        modeDescription.numberOfLines = 2
        modeDescription.attributedText = attributedModeDescriptionText
        modeDescription.textAlignment = .center
    }
}
import UIKit

class ModeSelectViewController: UIViewController {

    private var carouselView: UICollectionView!
    private var pageControl: UIPageControl!
    private var nextButton: UIButton!

    private let modeTitles = ["本格MIX", "一発録音"]
    private let modeDescriptions = ["弾き語りを本格録音!", "AI高音質レコーダーで\n一発録音!"]
    private let colorSets: [Dictionary<String, UIColor>] = [["circleOutside": UIColor(red: 239/255, green: 237/255, blue: 227/255, alpha: 1), "circleMiddle": UIColor(red: 211/255, green: 207/255, blue: 179/255, alpha: 1), "circleInside": UIColor(red: 142/255, green: 134/255, blue: 67/255, alpha: 1)], ["circleOutside": UIColor(red: 235/255, green: 228/255, blue: 240/255, alpha: 1), "circleMiddle": UIColor(red: 171/255, green: 182/255, blue: 200/255, alpha: 1), "circleInside": UIColor(red: 26/255, green: 68/255, blue: 103/255, alpha: 1)]]

    private var nextVCFlag: Int?

    override var shouldAutorotate: Bool {
        return false
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return UIInterfaceOrientationMask.portrait
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.navigationController?.navigationBar.isHidden = true
        self.view.backgroundColor = UIColor(red: 24/255, green: 65/255, blue: 99/255, alpha: 1)

        // force orientation change
        UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")

        setupCarouselView()
        self.view.addSubview(carouselView)

        setupPageControl()
        self.view.addSubview(pageControl)

        setupNextButton()
        self.view.addSubview(nextButton)
    }

    func setupCarouselView() {

        carouselView = UICollectionView(frame: CGRect(), collectionViewLayout: UICollectionViewFlowLayout()) // UICollectionView must be initialized with UICollectionViewFlowLayout()
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 0.0
        layout.minimumInteritemSpacing = 0.0
        layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        carouselView.collectionViewLayout = layout
        carouselView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height * 7 / 8)
        carouselView.frame.origin = CGPoint(x: 0, y: 0)

        carouselView.register(CarouselCell.self, forCellWithReuseIdentifier: "CarouselCell")

        carouselView.backgroundColor = .clear
        carouselView.clipsToBounds = false
        carouselView.isPagingEnabled = true
        carouselView.showsHorizontalScrollIndicator = false

        carouselView.delegate = self
        carouselView.dataSource = self
    }

    func setupPageControl() {
        pageControl = UIPageControl()
        pageControl.frame.size = CGSize(width: 120, height: 40)
        pageControl.center = CGPoint(x: self.view.center.x, y: self.view.center.y + 280)
    }

    func setupNextButton() {
        let attributedNextButtonText = NSAttributedString(string: "次へ →", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
        nextButton = UIButton()
        nextButton.frame.size = CGSize(width: 120.0, height: 64.0)
        nextButton.frame.origin.y = UIScreen.main.bounds.maxY - nextButton.frame.height * 2
        nextButton.frame.origin.x = UIScreen.main.bounds.maxX - nextButton.frame.width
        nextButton.setAttributedTitle(attributedNextButtonText, for: .normal)
        let path = UIBezierPath(roundedRect: nextButton.bounds, byRoundingCorners: [.bottomLeft, .topLeft], cornerRadii: CGSize(width: 32, height: 32))
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        nextButton.layer.mask = mask
        nextButton.backgroundColor = UIColor(red: 188/255, green: 164/255, blue: 69/255, alpha: 1)
        nextButton.addTarget(self, action: #selector(nextButtonDidTapped), for: .touchUpInside)
    }

    @objc func nextButtonDidTapped() {
        var nextViewController: UIViewController?
        if nextVCFlag == 0 {
            nextViewController = GuitarViewController()
        } else if nextVCFlag == 1 {
            nextViewController = OneShotViewController()
        } else {
            return print("next view controller not chosen")
        }
        self.navigationController?.pushViewController(nextViewController!, animated: true)
    }

}

// extensions below
extension ModeSelectViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        return CGSize(width: self.view.frame.width, height: carouselView.frame.height - 44)
    }
}

extension ModeSelectViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        nextVCFlag = indexPath.row
    }
}

extension ModeSelectViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

        let numberOfItems = 2
        pageControl.numberOfPages = numberOfItems

        return numberOfItems
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarouselCell", for: indexPath) as! CarouselCell
        configureCell(cell: cell, indexPath: indexPath)

        return cell
    }

    func configureCell(cell: CarouselCell, indexPath: IndexPath) {
        cell.modeTitleText = modeTitles[indexPath.row % 2]
        cell.modeDescriptionText = modeDescriptions[indexPath.row % 2]

        // setupUI()でproperty定義しているのでこの順番を変えぬよう
        cell.setupUI()

        cell.circleOutside.backgroundColor = colorSets[indexPath.row % 2]["circleOutside"]
        cell.circleMiddle.backgroundColor = colorSets[indexPath.row % 2]["circleMiddle"]
        cell.circleInside.backgroundColor = colorSets[indexPath.row % 2]["circleInside"]
    }
}

extension ModeSelectViewController: UIScrollViewDelegate {
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        pageControl.currentPage = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width)
    }

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        pageControl.currentPage = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width)
    }
}

おすすめ

人気の投稿

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です