‘캐러셀’은 한국어로 회전 목마라는 뜻입니다. 보통, 디자인 또는 개발과 관련된 영역에서는 이미지나 텍스트의 슬라이드를 가로로 슬라이드시켜 여러 개를 표시하는 컴포넌트를 말합니다. 타이머에 의해 자동으로 스크롤되는 캐러셀 배너를 구현하면서 겪은 이슈와 구현 방법을 공유합니다.

캐러셀 배너를 구현하기 위해서는 다음과 같은 전제 조건이 따릅니다. 수평 스크롤/ 페이징/ 배너의 x 오프셋에 대한 콜백, 이 3가지 조건을 취합할 때 iOS에서는 UICollectionView로 구현하는 것이 가장 최적의 선택이었습니다.
페이징에 대해서 설명드리겠습니다. 페이징은 앱에서 여러 페이지의 콘텐츠를 슬라이드 방식으로 구현한 구현체를 말합니다. 안드로이드의 경우, ViewPager라는 컴포넌트를 제공하기도 합니다. iOS의 UICollectionView는 isPagingEnabled를 통해 자연스러운 페이징 애니메이션을 제공합니다.
스크롤 오프셋은 배너의 자동 스크롤 구현에 있어서 핵심적인 역할을 합니다. 자세한 이야기는 직접 구현하면서 설명드리고, UICollectionView는 Rx에서 제공하는 contentOffset라는 옵저버블을 통해 현재 collectionView의 offset을 구독할 수 있습니다. 즉, offset 변화를 감지해 이에 대한 콜백을 받을 수 있다는 말입니다.
우선, 간단하게 배경 색과 페이지 넘버가 들어간 Cell을 구현합니다.
import UIKit
import SnapKit
struct BannerCellDataSource {
let text: String
let bgColor: UIColor
}
class BannerCell: UICollectionViewCell {
private lazy var textLabel = {
let view = UILabel()
view.font = .systemFont(ofSize: 24)
view.textAlignment = .center
view.textColor = .white
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
contentView.layer.cornerRadius = 16
}
private func layout() {
contentView.addSubview(textLabel)
contentView.snp.makeConstraints {
$0.verticalEdges.equalToSuperview()
$0.horizontalEdges.equalToSuperview().inset(16)
}
textLabel.snp.makeConstraints {
$0.center.equalToSuperview()
}
}
func setCell(_ item: BannerCellDataSource) {
textLabel.text = item.text
contentView.backgroundColor = item.bgColor
}
}
다음으로, Cell에 들어갈 DataSource를 ViewController에 선언해줍니다.
import UIKit
class ViewController: UIViewController {
let banners = [BannerCellDataSource(text: "1번 배너", bgColor: .black),
BannerCellDataSource(text: "2번 배너", bgColor: .darkGray),
BannerCellDataSource(text: "3번 배너", bgColor: .lightGray)]
override func viewDidLoad() {
super.viewDidLoad()
}
}
이제, CollectionView를 선언해주고, Cell을 등록합니다.
import UIKit
import SnapKit
class ViewController: UIViewController {
private let banners = [BannerCellDataSource(text: "1번 배너", bgColor: .black),
BannerCellDataSource(text: "2번 배너", bgColor: .darkGray),
BannerCellDataSource(text: "3번 배너", bgColor: .lightGray)]
private lazy var bannerCollectionView = {
let width = UIScreen.main.bounds.width
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: width, height: width * (9.0 / 16.0))
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.register(BannerCell.self, forCellWithReuseIdentifier: "BannerCell")
view.showsHorizontalScrollIndicator = false
view.showsVerticalScrollIndicator = false
view.isPagingEnabled = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
layouts()
}
private func layouts() {
view.addSubview(bannerCollectionView)
let width = UIScreen.main.bounds.width
bannerCollectionView.snp.makeConstraints {
$0.horizontalEdges.equalToSuperview()
$0.centerY.equalToSuperview()
$0.height.equalTo(width * (9/16))
}
}
}