관리 메뉴

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

[iOS - Swift] SelfSizingTableView, SelfSizingCollectionView 구현 방법 (Dynamic Size) 본문

iOS 응용 (swift)

[iOS - Swift] SelfSizingTableView, SelfSizingCollectionView 구현 방법 (Dynamic Size)

jake-kim 2022. 11. 26. 22:50

SelfSizing이 적용된 TableView

SelfSizing 아이디어

  • SelfSizing이란?
    • Cell의 크기에 따라 일종의 Container인 TableView와 CollectionView의 크기도 동적으로 커지게 만드는 것
  • 구현 아이디어
    • layoutSubviews()에서 invalidateIntrinsicContentSize() 호출
    • intrinsicContentSize를 재정의하여 콘텐츠의 크기만큼 해당 뷰가 늘어나게끔 구현
    • 사용하는쪽에서는 UIStackView로 위에서 정의한 tableView, collectionView를 넣어서 사용
      • UIStackView는 내부 뷰의 intrinsicContentSize에 따라 달라지므로, SelfSizing 구현에 적합
    • 여기까지 하면 자동으로 height가 변경되는 tableView, scrollView를 구현할 수 있고, UIScrollView안에 넣으면 스크롤까지 가능

LayoutSubviews()와 invalidateIntrinsicContentSize() 개념

  • layoutSubviews()
    • 뷰가 그려지는 Constraints -> Layout -> Draw 순서 중 Layout 중 하나이며, 해당 뷰의 크기나 레이아웃이 변경되면 하위 뷰(subview)들도 변경되어야 하므로 하위 뷰들에게 업뎃을 알림

  • invalidateIntrinsicContentSize() 
    • intrinsicContentSize를 업데이트하는 메소드

SelfSizing 구현

  • SelfSizingTableView, SelfSizingCollectionView 구현
class SelfSizingTableView: UITableView {
  override var intrinsicContentSize: CGSize {
    contentSize
  }
  
  override func layoutSubviews() {
    invalidateIntrinsicContentSize()
    super.layoutSubviews()
  }
}

class SelfSizingCollectionView: UICollectionView {
  override var intrinsicContentSize: CGSize {
    contentSize
  }
  
  override func layoutSubviews() {
    invalidateIntrinsicContentSize()
    super.layoutSubviews()
  }
}
  • (원리는 같으니 예제에서는 tableView를 중심으로 작성)
  • tableView는 동적으로 intrinsicContentSize가 증가하므로, UIStackView안에 넣어서 intrinsicContentSize만큼 자동으로 height값이 증가하도록 구현
class ViewController: UIViewController {
  private var items = (0...2).map { String($0) }
  private let stackView: UIStackView = {
    let stackView = UIStackView()
    stackView.axis = .vertical
    stackView.translatesAutoresizingMaskIntoConstraints = false
    return stackView
  }()
  private let tableView: SelfSizingTableView = {
    let view = SelfSizingTableView()
    view.allowsSelection = false
    view.backgroundColor = .clear
    view.separatorStyle = .none
    view.bounces = true
    view.showsVerticalScrollIndicator = true
    view.contentInset = .zero
    view.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
    view.estimatedRowHeight = 120
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
  }()
}
  • 데이터를 추가하며 테이블뷰의 height가 증가하는 것을 확인하기 위해 button과 label 준비
    • button: 누를 경우 아이템 증가
    • label: 테이블 뷰 밑에 있어서, 테이블 뷰 높이가 증가하는것을 확인하는 용도
  private let button: UIButton = {
    let button = UIButton(type: .system)
    button.setTitle("데이터 추가", for: .normal)
    button.addTarget(self, action: #selector(tap), for: .touchUpInside)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
  }()
  private let label: UILabel = {
    let label = UILabel()
    label.text = "테이블뷰 밑에 있는 Label"
    label.font = .systemFont(ofSize: 24)
    label.numberOfLines = 1
    label.textColor = .black
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()
  
  @objc private func tap() {
    let array = (Int(items.last!)!+1...Int(items.last!)!+2).map { String($0) }
    items.append(contentsOf: array)
    tableView.reloadData()
  }
  • 레이아웃
  override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(stackView)
    view.addSubview(button)
    view.addSubview(label)
    
    stackView.addArrangedSubview(tableView)
    tableView.dataSource = self
    
    NSLayoutConstraint.activate([
      stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      
      label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      label.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 16),
      
      button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -120)
    ])
  }
  • extension
extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    items.count
  }
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    cell.textLabel?.text = items[indexPath.row]
    cell.textLabel?.textColor = .white
    cell.backgroundColor = .random()
    return cell
  }
}

extension CGFloat {
  static func random() -> CGFloat {
    CGFloat(arc4random()) / CGFloat(UInt32.max)
  }
}

extension UIColor {
  static func random() -> UIColor {
    UIColor(
      red: .random(),
      green: .random(),
      blue: .random(),
      alpha: 1.0
    )
  }
}
  • 구현
    • 길이가 동적으로 늘어나고 있고 있지만, 화면의 height보다 더 크게 증가한 경우 스크롤이 안되는 이슈가 존재
    • UISCrollView를 사용하여 해결

  • UIScrollView를 이용하여 스크롤되게끔 구현
    • UIScrollView 안에 UIStackView를 넣으면, UIStackView의 내부 intrinsicContentSize에 따라서 자동으로 커지면 스크롤 되게끔 구현
  • scrollView 추가
  private let scrollView: UIScrollView = {
    let scrollView = UIScrollView()
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    return scrollView
  }()
  • tableView 스크롤 비활성화
  private let tableView: SelfSizingTableView = {
    let view = SelfSizingTableView()
	...
    view.isScrollEnabled = false // <-
    return view
  }()
  • addSubview
    • view에 scrollView를 넣고, scrollView에 stackView 삽입
  override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(scrollView) // <-
    scrollView.addSubview(stackView) // <-
    view.addSubview(button)
    view.addSubview(label)
  • 레이아웃
    • 핵심 - stackView의 leading, trailing 말고도 widthAnchor도 추가해야 콘텐츠가 표기
    • UIScrollView를 사용할때 세로 스크롤을 이용하고 싶으면 scroll의 width를 고정시켜야하고, 가로 스크롤을 이용하고 싶으면 height를 고정
  override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(scrollView) // <-
    scrollView.addSubview(stackView) // <-
    view.addSubview(button)
    view.addSubview(label)
    
    stackView.addArrangedSubview(tableView)
    tableView.dataSource = self
    
    NSLayoutConstraint.activate([
      scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
      scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
      scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
      scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
      
      stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
      stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
      stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
      stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
      stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), // 주의
      
      label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      label.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 16),
      
      button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -120)
    ])
  }

(결과)

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

* 참고

https://stackoverflow.com/questions/29779128/how-to-make-a-random-color-with-swift

Comments