畑田です。
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)
}
}