관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Infinite Carousel (무한 스크롤 뷰) 구현 방법 본문

iOS 응용 (swift)

[iOS - swift] Infinite Carousel (무한 스크롤 뷰) 구현 방법

jake-kim 2022. 12. 2. 23:22

Infinite ScrollView

구현 아이디어

  • 수평 스크롤을 위해서 UIScrollView를 이용해도 되지만, 데이터 소스 입력 편의를 위해 UICollectionView 사용
  • 무한 스크롤 원리 (데이터가 1,2,3 이렇게 있을 경우,)
    • 왼쪽에서 오른쪽으로 무한 스크롤: 
      • 데이터 세팅: 1, 2, 3, 1 (앞에있는걸 마지막에 붙이기)
      • scrollViewDidEndDecelerating에서 스크롤 된 크기를 알 수 있는 conttentOffset.x를 이용하여 1,2,3,1로 놓고 4번째 1에 도달했을때, 애니메이션 없이 다시 1로 돌아가도록 설정 
    • 오른쪽에서 왼쪽으로 무한 스크롤: 마찬가지로 conttentOffset.x를 이용하여 1,2,3,1로 놓고 1번째 1에 도달했을때, 애니메이션 없이 다시 1로 돌아가도록 설정 
      • 데이터 세팅: 3, 1, 2, 3 (뒤에있는걸 첫번째에 붙이기)
      • scrollViewDidEndDecelerating에서 스크롤 된 크기를 알 수 있는 conttentOffset.x를 이용하여 4,1,2,3로 놓고 1번째 3에 도달했을때, 애니메이션 없이 다시 3로 돌아가도록 설정
    • 양방향으로 구현해야하므로 첫번째와 마지막 아이템을 양쪽끝에 붙여서 구현 3,1,2,3,1
    • 주의) 델리게이트 메소드중 scrollViewDidEndDecelerating는 바로 불리지 않으므로 뷰가 보이자마자collectionView.setContentOffset을 4번으로 초기화 필요

Infinite Carousel 구현

  • UICollectionViewCell 구현
    • label 하나와 view 하나가 존재
final class CollectionViewCell: UICollectionViewCell {
  // MARK: UI
  private let someView: UIView = {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
  private let label: UILabel = {
    let label = UILabel()
    label.textColor = .white
    label.font = .systemFont(ofSize: 32)
    label.textAlignment = .center
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()
  
  // MARK: Initializer
  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.addSubview(someView)
    contentView.addSubview(label)
    
    NSLayoutConstraint.activate([
      someView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
      someView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
      someView.topAnchor.constraint(equalTo: contentView.topAnchor),
      someView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
    ])
    
    NSLayoutConstraint.activate([
      label.leadingAnchor.constraint(equalTo: someView.leadingAnchor),
      label.trailingAnchor.constraint(equalTo: someView.trailingAnchor),
      label.topAnchor.constraint(equalTo: someView.topAnchor),
      label.bottomAnchor.constraint(equalTo: someView.bottomAnchor)
    ])
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    prepare(nil, nil)
  }
  
  func prepare(_ color: UIColor?, _ text: String?) {
    someView.backgroundColor = color
    label.text = text
  }
}
  • ViewController 준비
import UIKit

class ViewController: UIViewController {
}
  • 아이템 준비
    • 1,2,3이 있고 랜덤 색상 하나로 튜플 형태의 아이템
  private var items = (1...3)
    .map { (String($0), [UIColor.gray, .red, .blue, .orange, .black].randomElement()) }
  • collectionView 준비
    • collectionView의 isScrollEnabled = true로 설정
  enum Metric {
    static let collectionViewHeight = 120.0
    static let cellWidth = UIScreen.main.bounds.width
  }
  
  private var items = (1...3)
    .map { (String($0), [UIColor.gray, .red, .blue, .orange, .black].randomElement()) }
  
  private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    layout.itemSize = .init(width: Metric.cellWidth, height: Metric.collectionViewHeight)
    layout.minimumLineSpacing = 0
    layout.minimumInteritemSpacing = 0
    return layout
  }()
  
  private lazy var collectionView: UICollectionView = {
    let view = UICollectionView(frame: .zero, collectionViewLayout: self.collectionViewFlowLayout)
    view.isScrollEnabled = true
    view.showsHorizontalScrollIndicator = false
    view.showsVerticalScrollIndicator = true
    view.contentInset = .zero
    view.backgroundColor = .clear
    view.clipsToBounds = true
    view.register(CollectionViewCell.self, forCellWithReuseIdentifier: "cell")
    view.translatesAutoresizingMaskIntoConstraints = false
    view.isPagingEnabled = true
    return view
  }()
  • viewDidLoad에서 아이템 변경 및 레이아웃
    • 아이템 변경 (1,2,3) -> (3,1,2,3,1)
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // as -is: 1 2 3
    // to -be: 3 1 2 3 1
    items.insert(items[items.count-1], at: 0)
    items.append(items[1])
    
    view.addSubview(collectionView)
    NSLayoutConstraint.activate([
      collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
      collectionView.heightAnchor.constraint(equalToConstant: Metric.collectionViewHeight)
    ])
    
    collectionView.dataSource = self
  }
  
extension ViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    items.count
  }
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
    cell.prepare(items[indexPath.item].1, items[indexPath.item].0)
    return cell
  }
}
  • 뷰가 보여질때 setContentOffset 값 변경 (현재 3,1,2,3,1이라 3의 화면을 보여지고 있으므로)
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    collectionView.setContentOffset(.init(x: Metric.cellWidth, y: collectionView.contentOffset.y), animated: false)
  }
  • scrollViewDidEndDecelerating에서 스크롤 처리
    • 3,1,2,3,1 데이터가 있을때,
      • 첫번째 3일이 보일땐 네번째 3으로 이동 (왼쪽에서 오른쪽으로 스크롤 헀을때 2가 나와야하므로)
      • 마지막 1이 보일땐 첫번째 1로 이동 (오른쪽으로 계속 스크롤 되는것처럼 보이기)
collectionView.delegate = self

extension ViewController: UICollectionViewDelegate {
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    let count = items.count
    
    if scrollView.contentOffset.x == 0 {
      scrollView.setContentOffset(.init(x: Metric.cellWidth * Double(count-2), y: scrollView.contentOffset.y), animated: false)
    }
    if scrollView.contentOffset.x == Double(count-1) * Metric.cellWidth {
      scrollView.setContentOffset(.init(x: Metric.cellWidth, y: scrollView.contentOffset.y), animated: false)
    }
  }
}

* 전체 코드: https://github.com/JK0369/ExInfiniteCarousel

* 참고

https://blog.naver.com/xodhks_0113/192122317

Comments