Notice
Recent Posts
Recent Comments
Link
관리 메뉴

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

[iOS - swift] UICollectionView 동적인 셀 구현 방법 (Dynamic Cell Size, Dynamic height) 본문

카테고리 없음

[iOS - swift] UICollectionView 동적인 셀 구현 방법 (Dynamic Cell Size, Dynamic height)

jake-kim 2023. 9. 1. 00:13

동적인 셀

  • UICollectionView나 UITableView를 사용할 때 동적인 셀 처리가 필요
  • Cell안에서 UIImageView에 URL을 가지고 image를 가져오는 피드와 같은 UI에 Cell의 height가 동적으로 변하는 경우가 존재

구현된 height가 동적인 UICollectionView

동적인 셀 구현 아이디어

  • 보통 이미지 라이브러리는 UIImageView의 extension으로 있고 이것을 아래처럼 사용 가능하도록 처리
    • 셀 안에서 setImage가 끝났을 때 동적으로 셀의 크기를 변경해주는 작업이 필요
// (Kingfisher 방법)
imageView.kf.setImage(with: url) { ... }

// (SDWebImage 방법)
imageView.sd_setImage(with: url) { ... }
  • 뷰를 오토레이아웃으로 처리한다면 Cell안에서 imageView의 size를 지정해놓고 이미지 로드가 완료된 시점에 update를 수행하면 imageView의 크기 업데이트
  • 주의사항 - imageView의 크기만 변경해주면 Cell자체의 크기가 변경되지 않음
    • imageView만 업데이트하면 Cell의 크기는 자동으로 업데이트 되지 않으므로 invalidateIntrinsicContentSize()를 호출하여 Cell의 크기에도 적용하면 완료

동적인 셀 구현

* 셀 구현 편의를 위해 SnapKit(오토레이아웃 편리), Kingfisher(이미지 캐싱) 사용

target 'ExDynamicHeight' do
  use_frameworks!
  
  pod 'SnapKit'
  pod 'Kingfisher'
end
  • 셀 정의
// MyCell.swift

import UIKit
import SnapKit
import Kingfisher

final class MyCell: UICollectionViewCell {

}
  • 필요한 imageView 선언 및 url 선언
// MyCell.swift

private let imageView = {
    let view = UIImageView()
    view.contentMode = .scaleAspectFill
    return view
}()

var url: URL? {
    didSet {
        // TODO
    }
}
  • UI 레이아웃 준비
// MyCell.swift

override init(frame: CGRect) {
    super.init(frame: frame)
    setupUI()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
    fatalError()
}

override func prepareForReuse() {
    super.prepareForReuse()
    url = nil
    imageView.image = nil
}

private func setupUI() {
    // TODO
}
  • setupUI() 구현
    • 가정) width값은 디바이스 크기 너비만큼 고정하고, height가 동적으로 변하도록 처리할 것
    • edges를 cell에 맞추고, 크기를 업데이트 시켜주기 위해서 width와 height를 선언한 후 priority로 오토레이아웃 충돌나지 않도록 설정
private func setupUI() {
    backgroundColor = .lightGray
    
    contentView.addSubview(imageView)
    imageView.snp.makeConstraints {
        $0.edges.equalToSuperview()
        $0.width.equalTo(UIScreen.main.bounds.width).priority(999)
        $0.height.equalTo(50).priority(999)
    }
}
  • url의 didSet 처리
    • 이미지 로드가 완료된 경우, imageView에 업데이트
    • 핵심) imageView만 업데이트하면 cell의 크기가 바뀌지 않으므로 invalidateIntrinsicContentSize()를 호출
var url: URL? {
    didSet {
        guard let url else { return }
        imageView.kf.setImage(with: url) { [weak self] result in
            guard case .success(let value) = result, let self else { return }
            imageView.snp.updateConstraints {
                $0.height.equalTo(value.image.size.height).priority(999)
            }
            invalidateIntrinsicContentSize()
        }
    }
}

 

동적인 셀 사용

  • 위에서 구현한 셀을 사용하는 ViewController에서는 별도 아무런 처리 없어도 손쉽게 사용이 가능
import UIKit
import SnapKit

class ViewController: UIViewController {
    private let collectionView = {
        let layout = UICollectionViewFlowLayout()
        let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
        view.register(MyCell.self, forCellWithReuseIdentifier: "cell")
        layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        layout.minimumInteritemSpacing = 10
        layout.minimumLineSpacing = 10
        view.contentInsetAdjustmentBehavior = .always
        return view
    }()
    
    private static var width: Int {
        Int(UIScreen.main.bounds.width)
    }
    private static var height: Int {
        (100...800).randomElement()!
    }
    
    var dataSource = (1...10)
        .compactMap { _ in URL(string: "https://random.imagecdn.app/\(ViewController.width)/\(ViewController.height)") }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.dataSource = self
        
        view.addSubview(collectionView)
        
        collectionView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        dataSource.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        print(dataSource)
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? MyCell
        cell?.url = dataSource[indexPath.row]
        return cell ?? UICollectionViewCell()
    }
}

(완성)

구현된 height가 동적인 UICollectionView

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

* 참고

https://github.com/onevcat/Kingfisher

https://github.com/SDWebImage/SDWebImage

Comments