본문 바로가기

iOS/Deep dive!!!

[iOS] Dynamic TableViewCell in Runtime(with animation) | 테이블뷰 Deep dive #1 사전 지식

 

안녕하세요. 

 

테이블 뷰를 기반으로 Runtime 때 사용자의 터치에 따라 dynamic한 tableView cell을 만들어 볼 것입니다. 이번 포스트에선 dynamic tableViewCell을 만들기 전에 알아두어야 할 사전 개념을 소개하려고 합니다 +_+

 

https://github.com/SHcommit/UIKitDeepDive/tree/master/DynamicTableViewCellWithAnimation

 


호호.. 재밌습니다.

 

 

 

이 화면을 구현할 때 중요한 키워드입니다. 

 

  • tableView contentView
  • UIView's intrinsicContentSize
  • view's priority + hugging | compression resistance
  • performBatchUpdates(_:completion:) vs beginUpdates() | endUpdates()
  • NSLayoutConstraint's priority in UILayoutPriority
  • 재사용 큐 문제
  • (애니메이션..?)

 

TableViewCell's contentView !

테이블 뷰 셀은 contentView가 있습니다.

https://developer.apple.com/documentation/uikit/uitableviewcell


contentView의 특징은 editing mode일 때 contentView가 애니메이션과 함께 밀려나면서 삭제 버튼이 왼쪽에 Delete Control로 추가됩니다. (아쉬운 점은 이 인스턴스의 이미지를 변경 등 직접 커스텀하기 힘들다는 것입니다. ㅠ 근데 계층구조에 있다면 -segmented control 커스텀 포스트- 이 포스트처럼 이미지는 변경 가능할수도 있을것 같다는 생각이 듭니다.)

 

 

이 경우 말고도 더 중요한 개념이 있습니다. Advanced Auto Layout 공식문서 링크

 

명확한 높이가 아니라 오토레이아웃으로 지정한 높이를 contentView가 반영한다는 것입니다.

 

let tableView = {
  let tableView = UITableView(frame: .zero, style: .plain)
  tableView.estimatedRowHeight = 70
  tableView.rowHeight = UITableViewAutomaticDimension
}()

 

이때 중요한 것은 estimatedRowHeight 값을 기반으로 셀의 높이를 계산합니다. 왠만하면 실제 보여질 cell의 높이와 근접한 값을 주는게 셀의 높이를 더 빨리 계산해서 렌더링 됩니다.

 

여기에 추가적으로 tableViewDelegate를 준수해야합니다.

 

      cf. 참고로 예전에 간단한 실험을 했습니다. Compositional layout을 기반으로 한 컬랙션 뷰 아이템의 heightDimension의 .estimated높이를 70으로 준 후 실제 컨텐츠 높이를 700으로 했을 때, 화면의 cell 높이가 70 크기 지정된 후 700으로 높이가 변경되는 현상을 봤습니다. 반면 실제 cell 컨텐츠 크기가 70과 유사한 경우 유사한 크기가 바로 적용됨을 알 수 있었습니다.

 

extension ViewController: UITableViewDelegate {
  func tableView(
    _ tableView: UITableView,
    heightForRowAt indexPath: IndexPath
  ) -> CGFloat {
    return UITableView.automaticDimension
  }
}

 

 

이렇게 하게 된다면 각각의 tableViewCell은 contentView의 subviews intrinsicContentSize.height 과 height constant등을 기반으로 self-sizing됩니다.

 

중요한 것은 tableViewCell의 self가아니라 contentView에 오토레이아웃을 해야합니다.

 

IntrinsicContentSize

여기서 중요한 개념은 intrinsicContentSize입니다.

 

UIView 타입은 intrinsicContentSize 프로퍼티를 오버라이딩해서 재정의 할 수 있습니다. UILabel, UIButton등은 텍스트를 입력할 경우 width, height를 지정하지 않아도 화면에 보여집니다. UILabel의 경우 텍스트가 변경될 때마다 intrinsicContentSize가 이에 맞게 자연스럽게 변경됩니다. 만약 변경되지 않는다면? 텍스트가 짤려서 linebreakmode발생될 수 있습니다.

하지만 특정 커스텀뷰 or UIView 등은 width, height를 명확하게 입력하거나, 오토레이아웃 잡지 않는다면 화면에 보여지지 않을 수 있습니다. width, height를 지정하지 않아도 UILabel처럼 화면에 보여주기 위해선 intrinsicContentSize를 오버라이딩해서 원하는 값을 정의할 수 있습니다.

 

이를 기반으로 content hugging, content compression resistance가 작동됩니다.

 

 

다시 돌아와서! 위에서 contentView에 오토레이아웃으로 subviews 위치를 잡을 경우 subview들의 높이를 기반으로 contentView cell이 self-resizing됩니다. 이때 intrinsicContentSize를 제공하지 않는 UIView타입이 있다면 오토레이아웃을 통해 height를 명시해줘야합니다. 또는 intrinsicContentSize를 오버라이드해서 해당 UIView 타입의 높이를 최소한으로 설정해야합니다.

 

TableView's performBatchUpdates(_:completion:)

테이블 뷰나 컬랙션 뷰에 적용할 수 있는 DiffableDataSource + Diffable snapshot은 통해 캡쳐된 데이터의 변경이 있을 경우(예를들어 데이터를 추가하거나 기존의 데이터가 사라져야 하는 경우 등) insert, delete 등의 작업이 있을 때 스무스한 애니메이션과 함께 처리됩니다. 물론 기본 제공되는 애니메이션말고 직접 커스텀할 수 있습니다. 그게바로 perfromBatchUpdates(_:completion:)입니다.

 

물론 디퍼블을 쓰지 않는 일반 테이블 뷰나 컬랙션 뷰에도 적용할 수 있습니다. 데이터의 변화가 필요한다던가 cell의 layout을 변형해야 한다던가의 경우 말입니다. iOS minimum target 6부터 사용될 수 있습니다. (iOS 6.0 이전에는 beginUpdates(), endUpdates()를 사용하면 됬었습니다.... 컼) 가능한 performBatchUpdates(_:completion:)을 사용하라고 합니다: )

 

reloadData는 테이블 뷰 전체를 재구성하기 때문에 효율이 좋지 못합니다. performBatchUpdates(_:completion:) updates 클로저에서 한 개나 한 개 이상으로 이루어진 batch를 효율적으로 업데이트 할 수있습니다. 근데 insert(at:with:), delete(at:with:)등의 메서드는 특정 IndexPath를 요구하는데 결국 이는 DataSource protocol을 기반으로 업데이트 된다는 것을 명심해야 합니다. insert(at:with:)을 사용할 때 [indexPaht]를 첫 매개변수로 넣었을 때 실제 DataSource에서 제공하는 데이터와 일치하는지가 중요합니다.

 

 

NSLayoutConstraint's priority 



다시 돌아와서!.. tableViewCell의 subviews들을 오토레이아웃과 constant를 기반으로 contentView에 대해서 제약을 걸었다고 가정하겠습니다. 위 사진에서 파랑색은 오토레이아웃을 잡은 모습입니다(NSLayoutCosntraint priority 미정). 이때 포스트 앞에서 언급한 estimatedHeight를 기반으로 self-resizing을 사용 했을때 오토레이아웃 충돌이 발생될 수 있습니다.

 

Try this: 
    (1) look at each constraint and try to figure out which you don't expect; 
    (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    ....
     "<NSLayoutConstraint:0x600002119950 'UIView-Encapsulated-Layout-Height' 
         UITableViewCellContentView:0x10651b720.height == 64.3333   (active)>"
)

 


'UIView-Encapsulated-Layout-Height' 제약조건과의 충돌...

 

이 충돌은 estimatedHeight를 autoDimension으로 했을 때 contentView.height를 명확하게 지정해도 발생됩니다. 그 이유는 스택 오버플로우(관련 링크)에서 찾을 수 있었습니다. 결국 self-resizing을 사용할 때에도 contentView의 전체 높이를 결정하는 NSLayoutConstraint의 priority나 contentView의 top, bottom에 제약을 거는 특정 NSLayoutCosntraint의 priority를 기본 지정 되는 우선순위 required(1000)보다 작은 값으로 지정해야합니다. 그래야만 self-resizing이 적용됩니다. (원래 priority가 defaultLow, high, required로 분류된건 높은 우선순위가 먼저 적용되는게 아닌가 의문이 들기도 하지만...)

 

물론 처음부터 self-resizing을 했을때 cell이 오토레이아웃 충돌이 나면 performBatchUpdates(_:completion:)을 통해 특정 cell 내부 subviews의 오토레이아웃 재배치 했을 때 이전의 contentView 높이가 달라졌을때도 높이를 결정짔는 subview의 top or bottom's constraint's priority를 required보다 낮추지 않는다면 이 또한 오토레이아웃 충돌이 발생됩니다.


이때도 마찬가지로 시스템 제약이 우선순위로 작동하게 되기 때문입니다.

 

TableView reusable dequeue problem !

마지막으로 재사용 문제입니다. dequeueReusableCell(withIdentifier:for:)를 통해 테이블 뷰 셀을 생성하거나 재사용 큐에서 꺼내올 때 주의해야 합니다. 

 

 

 

References:

https://stackoverflow.com/questions/15850417/cocoa-autolayout-content-hugging-vs-content-compression-resistance-priority

https://jcsoohwancho.github.io/2019-10-12-TableView%EC%9D%98-%EB%B3%80%ED%99%94%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95(1)-Batch-Update/


https://stackoverflow.com/questions/25059443/what-is-nslayoutconstraint-uiview-encapsulated-layout-height-and-how-should-i