관리 메뉴

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

[iOS - swift] 수평 스크롤 뷰와 커스텀 Indicator View (쿠팡 수평 스크롤 뷰, 스크롤 IndicatorView) 본문

UI 컴포넌트 (swift)

[iOS - swift] 수평 스크롤 뷰와 커스텀 Indicator View (쿠팡 수평 스크롤 뷰, 스크롤 IndicatorView)

jake-kim 2022. 6. 9. 22:30

수평 스크롤 뷰 + 커스텀 IndicatorView

쉽게 레이아웃을 구현하기 위해 사용한 프레임워크

구현 아이디어

  • UICollectionView를 사용하여 수평 스크롤 뷰 구현
  • 하단에 사용할 IndicatorView는 UIView를 서브클래싱하여 커스텀으로 구현
  • IndicatorView는 trackView와 trackTintView 두 개의 UIView로 구현
    • trackView - IndicatorView에서의 배경 UI
    • trackTintView - 스크롤 될 때 표시될 진행사항 UI
final class IndicatorView: UIView {
  // MARK: UI
  private let trackView = UIView().then {
    $0.backgroundColor = .lightGray.withAlphaComponent(0.3)
  }
  private let trackTintView = UIView().then {
    $0.backgroundColor = .gray
  }
  ...
}
  • trackView는 외부에서 autolayout을 정의해주는 크기대로 적용
  • trackTintView는 외부에서 collectionView의 크기에 따라 width가 변하도록 구현

IndicatorView의 길이는 외부에서 동적으로 사용가능하도록 구현

indicatorView를 길게
IndicatorView를 짧게

 

IndicatorView 구현

  • UIView를 서브클래싱하여 정의
import UIKit
import SnapKit
import Then

final class IndicatorView: UIView {
  // MARK: UI
  private let trackView = UIView().then {
    $0.backgroundColor = .lightGray.withAlphaComponent(0.3)
  }
  private let trackTintView = UIView().then {
    $0.backgroundColor = .gray
  }
}
  • 레이아웃
    • trackTintView는 외부에서 offset을 넣어주면 그에따라 이동되게 해야하므로 left에 관한 constraint가 계속 update되어야 하므로 전역변수에 leftInsetConstraint를 선언해놓고 사용
    • trackTintView는 trackView의 왼쪽과 오른쪽을 넘어가면 안되므로, left.greaterThanOrEqualToSuperview와 right.lessThenOrEqualToSuperview()로 정의해놓고, left.eqaulToSuperview()에는 priority를 작게 선언
private var leftInsetConstraint: Constraint?

override init(frame: CGRect) {
  super.init(frame: frame)
  
  self.addSubview(self.trackView)
  self.trackView.addSubview(self.trackTintView)
  
  self.trackView.snp.makeConstraints {
    $0.edges.equalToSuperview()
  }
  self.trackTintView.snp.makeConstraints {
    $0.top.bottom.equalToSuperview()
    $0.width.equalToSuperview().multipliedBy(1.0/5.0)
    $0.left.greaterThanOrEqualToSuperview()
    $0.right.lessThanOrEqualToSuperview()
    self.leftInsetConstraint = $0.left.equalToSuperview().priority(999).constraint
  }
}
  • 핵심 - 외부에서 widthRatio와 leftOffsetRatio을 받아서 trackTintView의 width값과 leftOffset을 업데이트할 수 있도록 프로핕를 선언
// MARK: Properties
var widthRatio: Double? {
  didSet {
    guard let widthRatio = self.widthRatio else { return }
    self.trackTintView.snp.remakeConstraints {
      $0.top.bottom.equalToSuperview()
      $0.width.equalToSuperview().multipliedBy(widthRatio)
      $0.left.greaterThanOrEqualToSuperview()
      $0.right.lessThanOrEqualToSuperview()
      self.leftInsetConstraint = $0.left.equalToSuperview().priority(999).constraint
    }
  }
}
var leftOffsetRatio: Double? {
  didSet {
    guard let leftOffsetRatio = self.leftOffsetRatio else { return }
    self.leftInsetConstraint?.update(inset: leftOffsetRatio * self.bounds.width)
  }
}

사용하는 쪽

  • UI 선언
class ViewController: UIViewController {
  private let collectionView = UICollectionView().then { ... }
  private let indicatorView = IndicatorView()
}
  • Constraint
self.collectionView.snp.makeConstraints {
  $0.centerY.left.right.equalToSuperview()
  $0.height.equalTo(Constant.collectionViewHeight)
}
self.indicatorView.snp.makeConstraints {
  $0.top.equalTo(self.collectionView.snp.bottom).offset(4)
  $0.left.right.equalTo(self.collectionView).inset(100)
  $0.height.equalTo(4)
}
  •  핵심1)
    • collectionView의 contentSize가 결정되는 viewDidAppear에서, contentSize를 이용하여 indicatorView의 width값을 업데이트
override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  let allWidth = self.collectionView.contentSize.width + self.collectionView.contentInset.left + self.collectionView.contentInset.right
  let showingWidth = self.collectionView.bounds.width
  self.indicatorView.widthRatio = showingWidth / allWidth
  self.indicatorView.layoutIfNeeded()
}

* 참고) contentOffset, contentInset, contentSize

  • contentOffset: 스크롤한 길이
  • contentInset: collectionView의 테두리 부분과의 여백 (4곳만 존재)
  • contentSize: 스크롤 가능한 콘텐츠 사이즈 (주의 - contentInset 값을 합해야, collectionView 전체 콘텐트 사이즈)
  •  핵심2)
    • 스크롤 될때마다 trackTintView의 위치를 변경해주어야 하므로, scrollViewDidScroll(_:)에서 스크롤 정보 업데이트
extension ViewController: UICollectionViewDelegate {
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let scroll = scrollView.contentOffset.x + scrollView.contentInset.left
    let width = scrollView.contentSize.width + scrollView.contentInset.left + scrollView.contentInset.right
    let scrollRatio = scroll / width
    
    self.indicatorView.leftOffsetRatio = scrollRatio
  }
}

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

Comments