본문 바로가기
iOS/예제

[iOS 예제] CircularCarouselBannerView

by Sky Titan 2022. 8. 19.
728x90

CircularCarouselBannerView

 아래 사진과 같이, 배달의 민족이나 기타 커머스 앱들에서 상단에 무한하게 순환하는 형태의 Carousel Banner를 확인할 수 있는데 이걸 만들고자 한다.

 

 조건은 Circular하게 스크롤했을 때 무한한 순환이 가능해야 되고, auto scroll이 되어야 한다.

 

 기본적인 원리는 아래 그림과 같이 item list의 앞 뒤에 맨 첫 번째 아이템, 맨 마지막 아이템을 하나씩 이어붙인 다음에 ScrollView의 가장 처음, 혹은 마지막에 도착했을 때 contentOffset을 이동시켜서 무한히 이동할 수 있는 것처럼 보이게 하는 것이다.

 

https://github.com/Sky-Titan/CircularCarouselBannerView

 

GitHub - Sky-Titan/CircularCarouselBannerView: iOS Swift CircularCarouselBannerView Example

iOS Swift CircularCarouselBannerView Example. Contribute to Sky-Titan/CircularCarouselBannerView development by creating an account on GitHub.

github.com

 

 

//
//  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

댓글