728x90
CircularCarouselBannerView
아래 사진과 같이, 배달의 민족이나 기타 커머스 앱들에서 상단에 무한하게 순환하는 형태의 Carousel Banner를 확인할 수 있는데 이걸 만들고자 한다.
조건은 Circular하게 스크롤했을 때 무한한 순환이 가능해야 되고, auto scroll이 되어야 한다.
기본적인 원리는 아래 그림과 같이 item list의 앞 뒤에 맨 첫 번째 아이템, 맨 마지막 아이템을 하나씩 이어붙인 다음에 ScrollView의 가장 처음, 혹은 마지막에 도착했을 때 contentOffset을 이동시켜서 무한히 이동할 수 있는 것처럼 보이게 하는 것이다.
https://github.com/Sky-Titan/CircularCarouselBannerView
//
// CircularCarouselBannerView.swift
// CircularCarouselBanner
//
// Created by 박준현 on 2022/08/19.
//
import UIKit
class CircularCarouselBannerView: UIView {
@IBOutlet weak var contentView: UIView!
@IBOutlet weak var scrollView: UIScrollView!
var images: [UIImage?] = [] {
didSet {
setupInnerItems()
}
}
var autoScrollDuration: Double = 3
var isAutoScroll: Bool = false
private var innerItems: [UIImage?] = [] {
didSet {
setupImageViews()
}
}
private var timer: Timer?
private var sectionWidth: CGFloat {
self.frame.width
}
private var sectionHeight: CGFloat {
self.frame.height
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
if Bundle.main.loadNibNamed("CircularCarouselBannerView", owner: self) != nil {
addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: self.topAnchor),
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
scrollView.delegate = self
}
}
deinit {
stopAutoScroll()
}
override func layoutSubviews() {
super.layoutSubviews()
setupImageViews()
}
private func setupInnerItems() {
self.innerItems.removeAll()
var items: [UIImage?] = []
if images.count > 1 {
items.append(images.last ?? UIImage())
items.append(contentsOf: images)
items.append(images.first ?? UIImage())
} else {
items.append(contentsOf: images)
}
// 앞뒤에 추가로 아이템을 이어붙인 image array
// [0 1 2 3] -> [3 0 1 2 3 0]
self.innerItems = items
}
private func setupImageViews() {
scrollView.subviews.forEach({
$0.removeFromSuperview()
})
var x: CGFloat = 0
for image in self.innerItems {
let imageView = UIImageView(image: image)
imageView.frame = CGRect(x: x, y: 0, width: sectionWidth, height: sectionHeight)
imageView.contentMode = .scaleAspectFill
scrollView.addSubview(imageView)
x += sectionWidth
}
//content size 지정
scrollView.contentSize.width = x
scrollView.contentSize.height = sectionHeight
// 첫번째 이미지 위치로 초기화
scrollView.contentOffset.x = sectionWidth
}
func startAutoScroll() {
stopAutoScroll()
timer = Timer.scheduledTimer(timeInterval: autoScrollDuration, target: self, selector: #selector(moveToRight), userInfo: nil, repeats: true)
}
func stopAutoScroll() {
timer?.invalidate()
timer = nil
}
@objc
private func moveToRight() {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x + sectionWidth, y: 0), animated: true)
}
}
extension CircularCarouselBannerView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 맨 앞에 도착한 경우
if scrollView.contentOffset.x <= 0 {
self.scrollView.contentOffset.x = CGFloat(images.count) * sectionWidth
}
// 맨 뒤에 도착한 경우
if scrollView.contentOffset.x >= CGFloat(innerItems.count - 1) * sectionWidth {
self.scrollView.contentOffset.x = sectionWidth
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
stopAutoScroll()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if isAutoScroll {
startAutoScroll()
}
}
}
다른 부분은 거의 다 뷰를 그리는 역할에 해당하고 순환 형태를 만드는 데 가장 중요한 부분이 scrollViewDidScroll안의 내용이다.
728x90
'iOS > 예제' 카테고리의 다른 글
[iOS 예제] SlideToUnlockView (0) | 2022.08.21 |
---|---|
[iOS 예제] UICollectionView에서 cell 수직방향 정렬 (0) | 2022.08.06 |
[iOS 예제] Recording Wave View 만들기 (0) | 2022.07.02 |
[iOS 예제] Roulette 만들기 (0) | 2022.06.25 |
[iOS 예제] Ripple Effect (0) | 2022.06.19 |
댓글