[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/UIKit

[UIKit] UITableView 기초부터 다시 살펴보기

inu 2022. 4. 17. 17:35
반응형

안녕하세요 이누입니다.

 

UITableView, UICollectionView 둘다 상황에 따라 많이 사용하는 View죠. 그런데 저는 지금까지 프로젝트에서 적용하기에만 급급해서인지 사용과정 및 원리가 머릿속에서 정리되어 있다는 느낌이 없었습니다. 그래서 이번기회에 둘 다 정리를 해보려고 합니다. 오늘은 먼저 TableView입니다!

 

이번 포스팅은 쓰다보니 좀 길어져서 목차도 첨부합니다. 아래 순서대로 설명할거예요.

  1. UITableView란?
  2. UITableView 생성하기
  3. UITableViewCell, UITableViewDataSource
  4. Cell Reuse?
  5. UITableViewDelegate

(이하 내용은 모두 코드베이스를 기준으로 설명되어 있습니다.)


1. UITableView란?

iOS의 UITableView는 하나의 열에 세로로 스크롤되는 콘텐츠 행들을 표시합니다. 스크롤을 할 수 있는 만큼 UIScrollView를 상속받고 있습니다. 테이블의 각 행에는 앱 콘텐츠의 일부분이 표함됩니다. 예를 들어 연락처앱은 각 연락처의 이름을 별도의 행에 표시합니다. 또 설정앱은 사용가능한 설정 그룹이 행으로 표시됩니다. 하나의 긴 행을 표시하도록 테이블을 구성하거나 관련 행을 섹션형태로 그룹화하여 콘텐츠를 더 쉽게 탐색할 수도 있습니다.

일반적으로 Navigation View Controller와 함께 사용하는 것이 일반적입니다. 이는 테이블의 다양한 계층의 탐색을 용이하도록 도와줍니다. UITableView는 각 행의 콘텐츠를 표시하는 cell로 구성됩니다. 표준 cell 구성은 텍스트와 이미지의 단순한 조합이지만 원할 경우 사용자 커스템 cell을 만들수도 있습니다. 또한 header, footer를 생성해 각 그룹에 대한 추가적인 정보를 제공할수도 있습니다.


2. UITableView 생성하기

이제 TableView가 무엇인지 알았으니 한번 생성해봅시다.

공식문서를 보면 TableView의 생성자는 2개가 존재함을 알 수 있습니다.

 

아래의 생성자는 unarchiver(스토리보드 혹은 xib 파일)를 기반으로 생성할 때 필요한 생성자이니 우리는 위의 생성자만 확인하면 됩니다. framer과 style을 지정해주면 됩니다. frame은 보통 AutoLayout을 통해 잡아줄거니까 .zero로 크기가 없도록 만들어도 상관없습니다. frame은 알겠는데 style은 뭘까요?

 

대충 스타일을 지정해주는 것 같기는 한데, 한번 UITableView.Style의 공식문서를 보겠습니다.

style에는 총 3가지가 존재하네요.

  • plain : 가장 기본적인 스타일
  • grouped : 각 섹션에 고유한 행들의 그룹이 있는 스타일
  • insetGrouped : 각 센션의 그룹이 동근 모서리 형태로 처리된 스타일

아아 원하는 형태에 따라 스타일을 줄 수 있는 옵션인 것 같네요!

 

정확히 어떻게 생겨먹은 친구들인지도 궁금해서 대충 데이터넣고 Section도 몇개 만들어서 형태를 비교해봤습니다. (이렇게 제대로 section까지 구분된 결과물을 확인하려면 DataSource를 통해 Cell 및 section을 설정하는 과정이 필요합니다. 지금은 그냥 해당 옵션이 어떻게 출력되는지 확인하기 위해 임의로 설정해놓은 상태이니 내부의 내용은 무시하고 형태만 봐주세요!)

plain grouped insetGrouped

오... 확실한 차이가 보이네요.

개인적으로 가장 최근에 나왔고 동글동글한 insetGrouped 옵션이 참 맘에 드는군요!

var tableView = UITableView(frame: .zero, style: .insetGrouped)

일단 UITableView 인스턴스를 생성하는 것까지는 성공했고,
이제부터 Cell과 DataSource라는 것들을 통해 TableView를 구성하는 방법에 대해 알아봅시다.


3. UITableViewCell, UITableViewDataSource

TableView에 데이터를 넣고 표시하기 위해서는 각 행을 표시해주는 UITableViewCell과 이를 관리하고 데이터를 적용해줄 수 있는 UITableViewDataSource가 필요합니다. 먼저 UITableViewCell이 무엇인지 알아봅시다.

UITableViewCell은 테이블 행의 내용을 관리하도록 도와주는 특수한 유형의 View입니다. 해당 View를 통해 각 행이 어떻게 보여질지 결정합니다.

 

UITableViewCell을 상속받아 Custom UITableViewCell를 구성하는 것으로 형태를 자유롭게 구성하는 것도 가능하지만, 이번 학습에서는 기본 UITableViewCell만을 사용하겠습니다.

 

자 이제 Cell이 뭔지 대충 알았으니 사용해봅시다. 사용할 때는 UITableViewDataSource가 필요해요.

UITableViewDataSource는 Cell에 데이터를 적용하고 이를 TableView에 넣어주는 역할을 합니다. 따로 객체를 통해 만들어줄 수도 있지만 가장 기본적인 방법은 ViewController를 UITableViewDataSource로 설정하고 사용하는 방법입니다. 이를 알아봅시다.

import UIKit

class ViewController: UIViewController {
    lazy var tableView = UITableView(frame: .zero, style: .insetGrouped)

    let data = [["Test 1-1","Test 1-2","Test 1-3","Test 1-4"],["Test 2-1","Test 2-2","Test 2-3"],["Test 3-1","Test 3-2"]]
    let header = ["Section 1","Section 2","Section 3"]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .white
        self.view.addSubview(self.tableView)
        self.tableView.dataSource = self

        self.tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            self.tableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
            self.tableView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
            self.tableView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
            self.tableView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }

}

그냥 현재 화면에 tableView를 추가하고 화면을 꽉차게해준 코드입니다. let data와 let header는 각 행에 들어갈 데이터, 헤더에 들어갈 데이터입니다. 여기서 중요한 것은 self.tableView.dataSource = self 이 부분입니다. 이 코드로 tableView의  dataSource로 self(=ViewController)를 할당해줬습니다.

 

근데 아무 작업없이 self를 그냥 지정해주면

이런 오류가 나옵니다. self가 UITableViewDataSource가 아니라서 할당할 수 없다는 뜻이죠! 이를 해결하기 위해서는 ViewController의 extension으로 ViewController에 UITableViewDataSource가 될 수 있는 코드를 추가해주어야 합니다.

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data[section].count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: .none)
        cell.textLabel?.text = data[indexPath.section][indexPath.row]
        return cell
    }
}

이렇게요.

 

UITableViewDataSource를 채택해주면 필수적으로 작성해야하는 메서드가 2개 있습니다. 바로 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Intfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)입니다.

 

func numberOfSections(in tableView: UITableView)는 TableView의 각 section이 몇개의 row를 포함시킬 것인지를 묻는 메서드입니다. 앞서 저희는 tableView에 들어갈 데이터를 미리 생성해놓았으니 이를 활용해서 값을 반환해주면 됩니다. (section은 현재 만들 section의 number를 의미합니다. 따라서 data[section].count를 활용하면 각 section의 데이터 개수를 받아올 수 있겠죠?)

 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)는 cell을 본격적으로 생성하는 메서드입니다. indexPath에는 현재 생성하려는 cell이 몇번째 section의 몇번째 row인지에 대한 정보가 담겨있습니다. 이를 활용하여 data[indexPath.section][indexPath.row]와 같이 데이터를 가져올 수 있겠죠? 이를 활용해 기본 UITableViewCell을 만들고 여기의 textLabel.text 속성에 해당 String 데이터를 할당해줬습니다.

 

(UITableViewCell 생성자의 reuseIndentifier가 신경쓰이시죠? 이 부분은 cell을 재사용할 때 사용되는 옵션입니다. 저는 일단 재사용관련된 부분은 신경쓰지 않으려고 .none으로 지정해줬습니다. cell 재사용에 대해서는 아래의 4번 섹션에서 다뤄볼게요)

 

여기까지 수행하면 아래와 같은 결과를 확인해볼 수 있습니다.

section 구분은 다 어디갔냐구요? 위 2개의 메서드는 tableView 구성을 위해 필수적인 코드일뿐, 모든 것을 해결해주지는 않습니다. section 구분을 위해서는 2가지 추가 메서드가 필요합니다.

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return header.count
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return header[section]
    }
    ...
}

func numberOfSections(in tableView: UITableView)func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int)입니다.

 

func numberOfSections(in tableView: UITableView)는 현재 TableView에 적용해줄 Section의 개수를 반환합니다. func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int)에서는 각 Section의 header 이름을 정의할 수 있습니다.

 

이렇게 2개의 메서드까지 추가해주고 나면...

이제 Section까지 적용되어서 아주 잘나오네요!


지금까지의 과정을 통해 TableView를 생성하고 DataSource 및 기본 Cell을 사용해 데이터를 표시하는 것까지는 성공했습니다.


4. Cell Reuse?

자 이제 데이터를 넣는 것까지는 성공했으니 TableView의 다양한 기능을 처리해주는 UITableViewDelegate에 대해 알아봐야하는데요... 그전에 UITableView와 UICollectionView 사용에 있어 매우 중요한 요소인 Cell Reuse라는 개념에 대해 알아보고 넘어갑시다!

 

UITableView와 UICollectionView에서는 메모리 효율을 위해 모든 Cell을 한번에 생성하지 않습니다. 저희가 만드는 TableVIew의 Cell이 10개 내외로 적으면 이를 한번에 메모리에 올려도 상관없습니다. 하지만 Cell이 1000개, 10000개가 넘어가면 어떻게될까요? 이를 모두 한번에 메모리에 올렸다간 시스템이 좀 힘들어하겠죠? 그래서 iOS의 TableView에서는 dequeue의 형식으로 화면에 표시될 cell만을 메모리에 올리고 스크롤로 새로운 cell이 보여져야하면, 기존에 올라가있던 cell 중 안보이도록 변한 cell을 내리고 새롭게 보여져야하는 cell을 올리는 행위를 반복합니다.

 

그런데 이렇게 하려니 또 문제가 발생합니다. cell을 내리고 올리는 과정에서 발생하는 오버헤드가 부담스러워진 것입니다. 스크롤을 할때마다 매번 cell 인스턴스를 메모리에서 올리고 내리고하면 당연히 시스템을 성능 중 일부를 잡아먹을 것이고 사용자 경험에도 안좋은 영향을 주겠죠? 이를 극복하기 위해 iOS는 현재 생성되어 있는 cell을 재사용할 수 있도록하기로 결정합니다. 형태가 같은 cell이라면 굳이 메모리에 내릴거없이 재활용하고 내용만 좀 바꿔서 보여주자는거죠.

친절하게도 공식문서에 Cell 재활용 관련 메서드를 한 곳에 모아 정리해놨습니다. 이들을 활용하면 cell을 재활용할 수 있습니다. 크게 registerdequeueReusableCell로 구분되어 있음을 확인할 수 있는데요.

 

먼저 register는 재활용할 cell의 타입을 String 형태의 identifier를 기반으로 확인하겠다는 것을 tableView에 알려주는 메서드입니다. 재활용시 이 identifier를 기준으로 재활용할 수 있는 cell이 현재 새로 넣어주는 cell과 동일한 타입인지 확인하게됩니다. 따라서 이를 미리 등록하여 추후 확인할 수 있도록 하는 것이죠. 공식문서에 2개가 소개되어 있는데 하나는 UINib를 기반으로, 하나는 AnyClass를 기반으로 등록할 수 있도록 합니다. 사용은 아래와 같이 수행합니다.

self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")

그리고 실질적으로 재활용할 cell을 찾아서 활용하는 메서드가 dequeueReusableCell입니다. withIdentifier에 앞서 등록한 identifier를 입력해주는 것으로 적절한 타입의 재사용 Cell을 가져올 수 있습니다. 입력된 identifier에 해당하는 Cell타입이 register를 통해 등록되지 않은 경우 에러를 발생시킬 수 있습니다. 사용은 아래와 같이 수행합니다.

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellWithIndexPath = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        cellWithIndexPath.textLabel?.text = data[indexPath.section][indexPath.row]
        return cellWithIndexPath
    }

 

cf. 여기서 dequeueReusableCell(withIdentifier: String, for: IndexPath) 와 dequeueReusableCell(withIdentifier: String)의 차이점은 무엇일까요? 이는 본문 흐름과 다소 동떨어져있는 내용이라 따로 작성했습니다.

 

궁금하신 분은 제 다음 포스팅인 [UIKit] dequeueReusableCell (withIdentifier:for:) vs (withIdentifier:)에서 확인해주세요.


5. UITableViewDelegate

자 이제 마지막 UITableViewDelegate입니다. 이는 TableView의 여러 기능을 대신 수행하는 역할을 합니다. UITableViewDelegate가 관리하는 기능은 다음과 같습니다.

  • Custom Header 및 Footer View를 만들고 관리합니다.
  • Row, Header 및 Footer에 대한 사용자 정의 높이를 지정합니다.
  • 더 나은 스크롤 지원을 위해 높이 추정치를 제공합니다.
  • Row에 Indent를 부여합니다.
  • Row 선택에 응답합니다.
  • Swipe를 포함한 action에 응답합니다.
  • table의 내용을 편집하는데 도움을 줍니다.

워낙 기능이 다양하니 다 살펴볼 수는 없고 Row 선택, Row 높이 지정정도만 해봅시다. 일단 delegate 지정은 dataSource랑 비슷하게 하면됩니다.

self.tableView.delegate = self

dataSource와 마찬가지로 아무 작업없이 self를 tableView의 delegate로 할당해주면 에러가 발생합니다. self(=ViewController)가 UITableViewDelegate가 아니기 때문이죠.

 

ViewController를 UITableViewDelegate로 지정해주기만 하면 문제가 해결됩니다.

extension ViewController: UITableViewDelegate {

}

이렇게요.

 

UITableViewDataSource와는 다르게 UITableViewDelegate는 필수적으로 작성해야하는 메서드는 없습니다. 그냥 필요한 동작을 찾아서 정의해주기만하면 됩니다. 저희는 Row 선택, Row 높이 지정만 해보기로 했죠? 일단 UITableViewDelegate 공식문서에서 먼저 Row 선택과 관련된 메서드를 찾아봅시다.

여기있네요. 이제 이를 UITableViewDelegate에 작성해봅시다.

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.cellForRow(at: indexPath)?.backgroundColor = .red
    }
}

저는 알아보기쉽게 한번 선택하면 cell의 backgroundColor를 빨간색으로 바꿔버리는 코드를 작성했습니다. 잘 작동하는지 확인해봅시다.

잘되네요! 이제 UITableViewDelegate를 통해 Row의 Height값까지 바꿔봅시다. 이번에도 UITableViewDelegate 공식문서에 찾아봅시다.

여기있네요!

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.cellForRow(at: indexPath)?.backgroundColor = .red
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}

이번에도 알아보기 쉽게 조금 큰값인 100을 줘봤습니다. 확인해볼까요?

높이도 잘 변경되었고 선택시 색도 잘 변경되네요!

 

tip. 제가 필요한 메서드를 모두 공식문서에 찾았죠? 이처럼 필요한 메서드는 공식문서를 통해 잘 찾을 수 있습니다. delegate와 같이 모든 메서드를 외울 수 없는 경우에는 더 유용하겠죠? 공식문서 적극활용합시다!!


참고

여기까지 UITableView를 기초부터 다시 알아보는 시간을 가져보았습니다. 다시 정리해보니 보여지는 시각도 다르고 느낌이 새롭네요.

얼른 UICollectionView도 뜯어봐야겠습니다.

 

긴 글 읽어주셔서 감사합니다!!

반응형