본문 바로가기
iOS/UIKit

[iOS] TableView의 모든 것

by BrickSky 2023. 6. 6.

0. 들어가기전


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

 

 

1. UI TableView 만들기 🛠️


공식문서에서 살펴보면 테이블 뷰를 만들기 위해서는 2가지 방법이 있다고 합니다!
우선 frame 방법은 우리가 사용하는 코드로 구현할 때 사용하는 생성자이고,
corder 방법은 스토리 보드로 구현할 때 사용하는 방법이라고 합니다. (넘어갈께여~)

 

 

2. UI TableView style 알아보기 🎨


이렇게 tableview를 만드는데는 3가지의 style가 있음을 알 수 있습니다.
(tmi… 예전에는 생각없이 사용했던 부분의 개념이지만,, 문제 상황은 여기👈에서 찾아볼 수 있어요)
 

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

 
 
 
 
 
 
 
 
 
 

💡 여기까지가 tableView의 인스턴스를 생성한 부분입니다❗️

이제 TableView의 큰 뼈대는 다 잡았다고 생각해도 될거같아요.
TableView에 데이터를 넣고 표시하기 위해서는 각 행을 표시해주는 UITableViewCell
이를 관리하고 데이터를 적용해줄 수 있는 UITableViewDataSource가 필요해요!
먼저 UITableViewCell이 무엇인지 알아봅시다.

 

 

3. UI TableView Cell 📦


UI TableView Cell은 UIView를 상속받아 사용하는데요!

UITableViewCell 객체는 단일 테이블 행의 콘텐츠를 관리하는 특수화된 뷰 유형입니다. 주로 앱의 사용자 정의 콘텐츠를 구성하고 표시하기 위해 셀을 사용하지만, UITableViewCell은 테이블 관련 동작을 지원하기 위한 특정한 맞춤화를 제공합니다.
- 셀에 선택 혹은 강조 색상 적용
- 세부사항 또는 공개 제어와 같은 표준 액세서리 뷰 추가
- 셀을 편집 가능한 상태로 변경
- 셀의 콘텐츠 들여쓰기를 통해 테이블에서 시각적 계층 구조 생성
앱의 콘텐츠는 셀의 대부분 영역을 차지하지만, 셀은 다른 콘텐츠를 위한 공간을 확보하기 위해 그 공간을 조정할 수 있습니다. 셀은 콘텐츠 영역의 뒷쪽에 액세서리 뷰를 표시합니다. 테이블을 편집 모드로 전환하면, 셀은 콘텐츠 영역의 앞쪽에 삭제 제어를 추가하고, 선택적으로 액세서리 뷰를 재정렬 제어로 교체합니다.

➡️ 공식문서를 번역한 것입니다.
 
정리하면! UITableViewCell은 테이블 행의 내용을 관리하도록 도와주는 특수한 유형의 View입니다.
해당 View를 통해 각 행이 어떻게 보여질지 결정합니다.

 

3-1. TableView Cell의 구조


기본적으로 테이블뷰 셀은 아래 이미지와 같이 크게 콘텐츠 영역액세서리뷰 영역으로 구조가 나뉩니다.

  • 콘텐츠 영역: 셀의 왼쪽 부분에는 주로 문자열, 이미지 혹은 고유 식별자 등이 입력됩니다.
  • 액세서리뷰 영역: 셀의 오른쪽 작은 부분은 액세서리뷰로 상세보기, 재정렬, 스위치 등과 같은 컨트롤 객체가 위치합니다.
class TableViewCell: UITableViewCell {
    
    static let identifier = "SettingTableViewCell"
    let titleLabel = UILabel()
    let nextButton = UIButton()

extension TableViewCell {
    func setStyle() {
        backgroundColor = .tvingBlack
        selectionStyle = .none
        
        titleLabel.do {
            $0.font = .tvingMedium(ofSize: 15)
            $0.textColor = .tvingGray2
        }
        
        nextButton.do {
            $0.setImage(UIImage.Image.nextImage, for: .normal)
        }
    }

세미나 때 사용한 코드의 일부를 가져와봤습니다. 위의 그림에서 알 수 있듯이 콘텐츠 영역에는 문자열이 들어가고 액세서리 뷰에는 컨트롤 객체에 해당하는 버튼이 들어가있다는 것을 확인할 수 있습니다!
(승찬이형한테 제대로 배웠다는 생각을 또 한번..🤭)
 

UITableView 객체는 DataSource와 Delegate가 없다면 정상적으로 동작하기 어려우므로 두 객체가 꼭 필요합니다!
 
MVC 패턴에 따라 DataSource는 애플리케이션의 데이터 모델(M)과 관련되어 있으며, Delegate는 테이블뷰의 모양과 동작을 관리하기에 컨트롤러(C)의 역할에 가깝습니다.
 
테이블뷰는 뷰(V)의 역할을 합니다DataSource와 Delegate 덕분에 테이블뷰를 매우 유연하게 만들 수 있습니다.
 
 
 

💡 이제는 UITableViewDataSource를 알아보러 가시죠!

 

 

4. UITableViewDataSource 📱


UITableViewDataSource는 Cell에 데이터를 적용하고 이를 TableView에 넣어주는 역할을 합니다.

테이블 뷰는 데이터의 표시만을 관리하며, 데이터 자체는 관리하지 않습니다. 데이터를 관리하기 위해서, UITableViewDataSource 프로토콜을 구현하는 데이터 소스 객체를 테이블에 제공해야 합니다. 데이터 소스 객체는 테이블로부터 데이터 관련 요청에 응답합니다. 또한, 테이블의 데이터를 직접 관리하거나, 앱의 다른 부분과 협조하여 데이터를 관리합니다.

  • 테이블의 섹션과 행의 수를 보고합니다.
  • 테이블의 각 행에 대한 셀을 제공합니다.
  • 섹션 헤더와 푸터의 제목을 제공합니다.
  • 기본 데이터의 변경을 요구하는 사용자 또는 테이블이 초기화한 업데이트에 응답합니다.

➡️ 공식문서를 번역한 것입니다.
 

 

4-1. UITableViewDataSource가 하는 일!


  1. 데이터 소스는 테이블 뷰를 생성하고 수정하는데 필요한 정보를 테이블뷰 객체에 제공합니다.
  2. 데이터 소스는 데이터 모델의 델리게이트로, 테이블뷰의 시각적 모양에 대한 최소한의 정보를 제공합니다.
  3. UITableView 객체에 섹션의 수와 행의 수를 알려주며, 행의 삽입, 삭제 및 재정렬하는 기능을 선택적으로 구현할 수 있습니다.
💡 즉! TableView에 필요한 Data와 관련된 일을 한다고 생각하면 되겠죠?

 

4-2. 코드로 보시죠

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)
        ])
    }

}

et data와 let header는 각 행에 들어갈 데이터, 헤더에 들어갈 데이터입니다.
여기서 중요한 것은 self.tableView.dataSource = self 이 부분입니다. 이 코드로 tableView의  dataSource로 self(=ViewController)를 할당해줬습니다.

이런 오류가 나옵니다. 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개 있어요. 보시죠!
 

4-3. UITableViewDataSource를 체택하면 필요한 메서드

func numberOfSections(in tableView: UITableView)

위의 메서드는 TableView의 각 section이 몇개의 row(열)를 포함시킬 것인지를 묻는 메서드입니다.
앞서 tableView에 들어갈 데이터를 미리 생성해놓았으니 이를 활용해서 값을 반환해주면 됩니다.

그림처럼!
나누어진 경계가 섹션이에요.
 
 
 
 
 
 
 
 
 
 
 
 
 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

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

이렇게 각각의 row에 값이 들어간 것을 알 수 있어요!
 
 
 
 
 
 
 
 
 
 
 
 
 

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

 

 

5. TableView를 다시 생각해보자! 🤔


근데!! TableView를 사용하는 이유를 생각해보면,,
무수히 많은 셀을 직접 만들 수 없기 때문에 재사용하기 위해서 만드는거자나여??

(승찬이형 세미나 자료를 퍼왔습니다..ㅎㅎ)

이때 필요한 개념이 바로 Cell Resue입니다. 보러 가시죠❗️

 

 

6. Cell Reuse ♻️


🧐 근데!! TableView를 사용하는 이유를 생각해보면,,

메모리 효율을 위해 모든 Cell을 한번에 생성하지 않는거잖아요?? TableVIew의 Cell이 10개 정도로 적으면 한번에 메모리에 올려도 상관없겠지만… Cell이 1000개, 10000개가 넘어가면 어떻게될까요?

이를 모두 한번에 메모리에 올렸다간 시스템이 좀 힘들어하겠죠? 😅

그래서 iOS의 TableView에서는 dequeue의 형식으로 화면에 표시될 cell만을 메모리에 올리고 스크롤로 새로운 cell이 보여져야하면, 기존에 올라가있던 cell 중 안보이도록 변한 cell을 내리고 새롭게 보여져야하는 cell을 올리는 행위를 반복하는겁니다. 위의 사진에서 알수 있겠죠??

⛔️ 그런데 이렇게 하려니 또 문제가 발생합니다!

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

오버헤드란?
오버헤드란 프로그램의 실행흐름에서 나타나는 현상중 하나로 예를 들어 , 프로그램의 실행흐름 도중에 동떨어진 위치의 코드를 실행시켜야 할 때 , 추가적으로 시간,메모리,자원이 사용되는 현상입니다.한마디로 정의하자면, 오버 헤드는 특정 기능을 수행하는데 드는 간접적인 시간, 메모리 등 자원을 말한다.
 

공식문서에 Cell 재활용 관련 메서드를 확인할 수 있습니다. 이들을 활용하면 cell을 재활용할 수 있습니다.
크게 register와 dequeueReusableCell로 구분되어 있음을 확인할 수 있는데요.
먼저 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
    }

 
 
 

7. UITableViewDelegate 🚀


UITableViewDelegate는 테이블 뷰에서 선택 관리, 섹션 헤더 및 푸터 설정, 셀 삭제 및 재정렬, 그리고 다른 작업을 수행하는 메소드를 제공합니다.

  • 사용자 정의 헤더와 푸터 뷰를 생성하고 관리합니다.
  • 행, 헤더, 푸터의 사용자 정의 높이를 지정합니다.
  • 더 나은 스크롤 지원을 위해 높이 추정치를 제공합니다.
  • 행 내용을 들여 씁니다.
  • 행 선택에 응답합니다.
  • 테이블 행에서 스와이프 및 기타 액션에 응답합니다.
  • 테이블의 내용 편집을 지원합니다.

➡️ 공식문서를 번역한 것입니다.
 

self.tableView.delegate = self

이런 식으로 작성해주면 될까요?? 그렇지 않습니다!
self(=ViewController)가 UITableViewDelegate가 아니기 때문이죠.
 

extension ViewController: UITableViewDelegate {

}

이렇게! ViewController를 UITableViewDelegate로 지정해주기만 하면 문제가 해결됩니다.
위의 UITableViewDataSource와 달리 UITableViewDataSource의 경우 필수적으로 작성해야할 메서드는 없어요! 그냥 필요한 것들을 찾아서 사용하면 됩니다..^^
 

private func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> CGFloat {
        return 50
    }

저는 이런식으로 실제 코드에서 사용해보았었습니다.
 
 

8. 마지막 정리 ⛔️


 

 

9. 실습해보기 ✍️

 
이걸 한번 만들어볼께요!
(쉽게 쉽게 구분만 할 수 있을 정도로^^)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

#1 우선 TableViewController 파일을 만들면 다음과 같아요.

 

#2 각각의 셀에 들어갈 형식을 잡아주었습니다.

import UIKit

import SnapKit
import Then

class MyTableViewCell: UITableViewCell {
    
    static let identifier = "MyTableViewCell"
    
    private let settingIcon = UILabel()
    private let settingLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        setStyle()
        setHierarchy()
        setLayout()

    }
    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    func setStyle() {
        settingIcon.do {
            $0.font = UIFont.systemFont(ofSize: 15)
        }
        
        settingLabel.do {
            $0.font = UIFont.systemFont(ofSize: 15)
        }
        backgroundColor = .darkGray
    }
    func setLayout() {
        settingIcon.snp.makeConstraints {
            $0.leading.equalToSuperview().inset(15)
        }
        settingLabel.snp.makeConstraints {
            $0.leading.equalTo(settingIcon.snp.trailing).offset(20)
        }
    }
    
    func setHierarchy() {
        contentView.addSubviews(settingIcon,settingLabel)
    }
    
    func configureCell(_ setting: Setting) {
        settingIcon.text = setting.settingIcon
        settingLabel.text = setting.settingLabel
    }
}

 
 

#3 ViewController 부분을 집중적으로 봅시다 - 선언 부분

import UIKit

class MyTableViewController: UIViewController, UITableViewDelegate {
    
    private let tableView = UITableView(frame: .zero, style: .insetGrouped)
    private let dummy = Setting.dummy()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setStyle()
        setLayout()
    }

이 부분에서는 tableView의 style를 지정해 주었고, 추후 cell에 넣을 dummy를 선언해주었습니다.
그리고 viewDidLoad부분에 setStyle()과 setLayout()을 올려줌으로써 보이게 했습니다.
⛔️ 이 부분 항상 놓치고 넘어가는데 유의하자!!

 

#4 ViewController 부분을 집중적으로 봅시다 - style과 layout 그리고 재사용⭐️

func setStyle() {
        
        view.backgroundColor = .black
        
        self.navigationController?.navigationBar.isHidden = true
        
        tableView.do {
            $0.register(MyTableViewCell.self, forCellReuseIdentifier: MyTableViewCell.identifier)
            $0.rowHeight = 50
            $0.delegate = self
            $0.dataSource = self
            $0.backgroundColor = .black
        }
    }
    
   func setLayout() {
        view.addSubview(tableView)
        
        tableView.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide)
            $0.bottom.leading.trailing.equalToSuperview()
        }
    }

func setStyle() 함수를 통해 배경색을 지정해 주었고, navigationBar를 숨김처리 했습니다.
 
가장 중요한 부분이 바로 아래 부분입니다.

이 코드가 의미하는 것은 다음과 같습니다.

  • MyTableViewCell을 재사용하겠습니다!
  • forCellReuseIdentifier은 MyTableViewCell에 접근해서 cell에 들어갈 값을 가져오겠습니다.

 
=self를 통해 delegate와 detaSource의 주체가 tableView임을 명시했어요
 
또한 setLayout를 통해 tableView의 레이아웃을 잡아주었습니다.

 

#5 ViewController 부분을 집중적으로 봅시다 - UITableViewDataSource 적용

// MARK: UITableViewDataSource를 체택하면 필요한 메서드
extension MyTableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: MyTableViewCell.identifier, for: indexPath) as? MyTableViewCell else { return UITableViewCell() }
        cell.configureCell(dummy[indexPath.section][indexPath.row])
        return cell
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 0:
            return dummy[section].count
        case 1:
            return dummy[section].count
        default:
            return 0
        }
    }
}

UITableViewDataSource가 뭘 한다구요?
1️⃣ cell은 몇개를 그릴지  2️⃣ cell에는 어떤 내용을 담아서 보여줄지  3️⃣ section의 타이틀은 뭘로 할지 정해요..
 

이 부분에서는 재사용 가능한 cell을 요청해서 추후 생기게될 cell을 대비하는 부분? 이라고 생각하겠습니다.
 

dummy로 지정해둔 부분에서 section과 row를 그대로 가져오겠습니다! 하는 부분입니다.
 

dummy를 만든 부분에서 분리한거 보이시죠??
이걸 의미합니다.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

몇개의 섹션을 만들 것인지 지정해주었습니다.
 

마지막으로 반환하는 부분을 코드로 작성했습니다.
section 파라미터는 dummy[section]의 수를 반환하도록 했습니다.
 

그러면!!!! 완성!!! 됩니다…ㅎㅎ 그치만,,, 조금 아쉬우니까..

 

 

10. 조금 아쉬우니까.. 👻


cell 관련 속성은 UITableViewDelegate에서 관리하고 위에서 선언해두었으니까 바로 함수로 빼서 쓸게요.
 

#1 didSelectRowAt 을 공부해서 터치될때 색상을 바꿔볼래요!!

터치 되면 적용되는 코드겠죠? 터치를 끝내면 어떻게 할지는 뒤에서 작성하기로해요~
 

한번 터치하고 눌러도 색이 바뀌지 않아요..
이제 이걸 바꾸러 가봅시다!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

#2 didDeselectRowAt 을 공부해서 터치가 끝난 후 색상을 바꿔볼래요!!

터치가 된 후에 변경될 색상에 관한 코드에요!   

완성!!!!!!!!