[iOS] UITableView

설정

  1. Device Orientation : Portrait
  2. ViewController → ChecklistViewController ChecklistViewController: UITableViewController
  3. 스토리보드 Class: ChecklistViewController

UITableViewDataSource

TableView와 표시하고자 하는 Data간의 링크 개념

테이블 뷰는 표시해야할 데이터의 수 ( 행의 수 )와 그 행에 어떤 데이터를 표시해야하는지를 알아야함

이러한 정보를 Delegate패턴을 사용하여 DataSource로 알려준다.

Delegate method

override func tableView(_:,numberOfRowsInSection: Int) -> Int

TableView가 몇개의 행을 그려하는지 이 메서드를 통해 ViewController에게 물어보고

ViewController는 이 메서드의 구현을 통해 TableView에게 답함

override func tableView(_:, cellForRowAt:) -> UITableViewCell

TableView가 n번째 행에 어떤 데이터를 표시해야하는지 ViewController에게 물어보고

ViewController는 표시해야하는 데이터를 UITableViewCell 인스턴스를 생성하여 TableView에게 전달

/// 지정된 행(row)이 테이블 뷰의 다른 위치로 이동할 수 있는지 여부를 결정
/// - Parameters:
///   - tableView: 이 메소드를 호출한 테이블 뷰
///   - indexPath: 움직일 수 있는 여부를 알고 싶은 행의 indexPath
/// - Returns: 이동할 수 있으면 true, 그렇지 않으면 false
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
    if indexPath.section != snapshot().indexOfSection(.readMe) { return false }
    else {
        return true
    }
}
    /// 테이블 뷰의 특정 위치에 있는 행(row)를 다른 위치로 이동하도록 데이터 소스에 전달
    /// - Parameters:
    ///   - tableView: 이 메소드를 호출한 테이블 뷰
    ///   - sourceIndexPath: 이동할 행의 IndexPath
    ///   - destinationIndexPath: 이동 도착지의 IndexPath
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        code
    }

UITableViewDelegate

행을 터치하거나 행을 수정, 삭제등의 TableView 관련 기능 및 이벤트에 대한 프로토콜

Delegate method

func tableView(_:, didSelectForRowAt: IndexPath)

어떤 행이 터치 이벤트가 발생되었는지 IndexPath로 전달되어짐

ViewController는 터치 이벤트 시 처리해야할 로직을 작성할 수 있음

/// 해당 프로토콜 메서드를 구현하면 스와이프 삭제를 활성화,
/// - Parameters:
///   - tableView: 이 메서드를 호출한 tableView
///   - editingStyle: none, delete, insert 3가지
///   - indexPath: 이 이벤트를 호출한 행, indexPath
override func tableView(_ tableView: UITableView,
                        commit editingStyle: UITableViewCell.EditingStyle,
                        forRowAt indexPath: IndexPath
)
/// 특정 행을 선택하려 할 때 호출되는 메소드
/// - Parameters:
///   - tableView: 선택될 행의 TableView
///   - indexPath: 선택될 행의 IndexPath
/// - Returns: 최종 선택할 행의 IndexPath
override func tableView(_ tableView: UITableView,
                        willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    return nil
}

nil을 반환하면 선택되지 않음

/// 악세사리 버튼이 탭되었을 때 호출되는 메서드
/// - Parameters:
///   - tableView: 이 메서드를 호출한 tableview
///   - indexPath: 이 이벤트를 호출한 행
override func tableView(_ tableView: UITableView,
                        accessoryButtonTappedForRowWith indexPath: IndexPath) {
    let list = lists[indexPath.row]
    let vc = storyboard?.instantiateViewController(withIdentifier: "ListDetailViewController") as! ListDetailViewController
    vc.delegate = self
    vc.checklistToEdit = list
    navigationController?.pushViewController(vc, animated: true)
}

기타 관련 메소드와 프로퍼티

indexPathForSelectedRow

    guard let indexPath = tableView.indexPathForSelectedRow else { }

테이블뷰에서 선택한 행과 섹션을 식별하는 IndexPath

insertRows(at:[IndexPath], with:UITableView.RowAnimation)

func insertRows(at indexPaths: [IndexPath],
            with animation: UITableView.RowAnimation)

indexPaths : 추가할 아이템들의 IndexPath 배열

animation : 추가 시 보여질 애니메이션

주의 : TableView는 보통 데이터 모델과 연동하여 사용하는데 추가할 때는 데이터 모델과 테이블 뷰 모두에 추가되어야함, 데이터 모델과 테이블 뷰는 항상 동기화 되어야함

allowsSelection

tableView.allowsSelection = false 
// iOS 14 이후 .selectionFollowsFocus 도 지원

행이 선택 되었을 때 색을 변경할 건지?에 대한 프로퍼티로 추정

deleteRows(at: [indexPath], with:UITableView.RowAnimation)

func deleteRows(at indexPaths: [IndexPath], 
            with animation: UITableView.RowAnimation)

indexPaths : 삭제할 아이템들의 IndexPath 배열

animation : 삭제 시 보여질 애니메이션

주의 : TableView는 보통 데이터 모델과 연동하여 사용하는데 추가할 때는 데이터 모델과 테이블 뷰 모두에 추가되어야함, 데이터 모델과 테이블 뷰는 항상 동기화 되어야함

deselectRow(_ row: Int)

func deselectRow(_ row: Int)

row 번째 행의 행 선택을 취소함

indexPath(for: UITableViewCell)

func indexPath(for cell: UITableViewCell) -> IndexPath?

전달된 cell의 IndexPath를 반환

dequeueReusableCell(withIdentifier: String, for: IndexPath) → UITableViewCell

func dequeueReusableCell(withIdentifier identifier: String,
                                            for indexPath: IndexPath) -> UITableViewCell

indexPath의 행에 재사용하기 위한 식별자 identifier 를 가진 cell을 반환

register(_:AnyClass?, forCellReuseIdentifier: String)

func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)
func register(_ cellClass: UINib?, forCellReuseIdentifier identifier: String)

재사용하기 위한 cell을 식별자 identifier와 함께 등록. 추후 dequeueReusableCell에서 재사용 가능

Cell

Accessory type

  1. None
  2. Checkmark
    • cell의 accessoryType 프로퍼티에 .checkmark를 하면 체크, .none 으로 할당하면 체크가 없어짐 cell.accessoryType = item.checked ? .checkmark : .none
  3. Detail

셀을 생성하는 4가지 방법

  1. 다이나믹 프로토타입 셀 스토리 보드에서 바로 셀을 구성하여 사용 간단하고 빠름
  2. 정적 셀 (static cell) 어떤 셀을 가질지 미리 알고 있음 DataSource 메소드를 제공할 필요가 없음 (ex : cellForRowAt)
  3. Nib file UITableViewCell 오브젝트만 포함하는 스토리보드와 비슷함 스토리보드 외부에서 사용한다는 점을 제외하면 다이나믹 프로토타입 셀을 사용하는 것과 유사함
  4. 하드 코딩 특정 셀 스타일을 지정할 수 있음 미리 Label, ImageView가 구성된 레이아웃이 있는 셀을 사용할 수 있음

HeaderView, FooterView

기본 HeaderView, FooterView

헤더뷰

    /// 테이블 뷰 섹션의 헤더 title에 대한 데이터 소스 요청
    /// TableView DataSource 메소드
    /// - Parameters:
    ///   - tableView: title 을 묻는 테이블 뷰
    ///   - section: tableview의 섹션을 식별하는 인덱스 번호
    /// - Returns: 해당 섹션의 헤더 title로 사용할 문자열. nil일 경우 title은 없음
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return section == 1 ? "Read Me!" : nil
    }    

푸터뷰

    /// 테이블 뷰 섹션의 푸터 title에 대한 데이터 소스 요청
    /// TableView DataSource 메소드
    /// - Parameters:
    ///   - tableView: title 을 묻는 테이블 뷰
    ///   - section: tableview의 섹션을 식별하는 인덱스 번호
    /// - Returns: 해당 섹션의 푸터 title로 사용할 문자열. nil일 경우 title은 없음
    override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
    }

Custom HeaderView, FooterView

  1. 사용자 정의 nib 파일로 UI를 구성 후 클래스를 구현
// Custom 헤더뷰를 만들기 위한 클래스
class LibraryHeaderView: UITableViewHeaderFooterView {
    static let reuseIdentifier = "\(LibraryHeaderView.self)"
    @IBOutlet var titleLabel: UILabel!
}
  1. 헤더뷰를 재사용하기 위해 reuseIdentifier를 static 프로퍼티로 정의 타이틀을 설정하기 위한 titleLabel 선언, nib파일에서 Outlet 연결
    tableView.register(UINib(nibName: "\(LibraryHeaderView.self)", bundle: nil),
                    forHeaderFooterViewReuseIdentifier: LibraryHeaderView.reuseIdentifier)
  1. 헤더뷰를 재사용하기 위해 테이블 뷰에 등록.
        tableView.register(UINib(nibName: "\(LibraryHeaderView.self)", bundle: nil),
                        forHeaderFooterViewReuseIdentifier: LibraryHeaderView.reuseIdentifier)
  1. 해당 섹션에 맞는 헤더뷰를 재사용
    /// 테이블 뷰 섹션의 해더뷰에 대한 데이터 소스 요청
    /// TableView Delegate 메소드
    /// - Parameters:
    ///   - tableView: 사용할 헤더뷰를 묻는 테이블 뷰
    ///   - section: tableview의 섹션을 식별하는 인덱스 번호
    /// - Returns: 해당 섹션의 해더뷰로 사용할 UIView. nil일 경우 없음
    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        print("1")
        if section == 0 { return nil }

        guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: LibraryHeaderView.reuseIdentifier) as? LibraryHeaderView else {
            return nil
        }
        headerView.titleLabel.text = "Read Me!"
        return headerView
    }

    /// 테이블 뷰의 해당 섹션에 대한 헤더뷰 영역에 대한 높이를 요청
    /// TableView Delegate 메소드
    /// - Parameters:
    ///   - tableView: 메소드를 호출한 테이블 뷰
    ///   - section: tableview의 섹션을 식별하는 인덱스 번호
    /// - Returns: 해당 섹션의 헤더의 높이를 지정하는 음수가 아닌 부동 소수점 값
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return section != 0 ? 60 : 0
    }

위 소스에서 dequeueReusableHeaderFooterView(withIdentifier:) 메소드로 헤더뷰 재사용

코드

ChecklistItem.swift

/// Checklist의 데이터 모델
/// text : 할 일
/// checked : 선택 유무 
class ChecklistItem {
    var text = ""
    var checked = false
}

ChecklistViewController.swift

class ChecklistViewController: UITableViewController {
    /// Checklist의 데이터 모델
    var items = [ChecklistItem]()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Replace previous code with the following
        let item1 = ChecklistItem()
        item1.text = "Walk the dog"
        items.append(item1)

        let item2 = ChecklistItem()
        item2.text = "Brush my teeth"
        item2.checked = true
        items.append(item2)

        let item3 = ChecklistItem()
        item3.text = "Learn iOS development"
        item3.checked = true
        items.append(item3)

        let item4 = ChecklistItem()
        item4.text = "Soccer practice"
        items.append(item4)

        let item5 = ChecklistItem()
        item5.text = "Eat ice cream"
        items.append(item5)
    }
    
    func configureText(for cell: UITableViewCell, with item: ChecklistItem) {
        let label = cell.viewWithTag(1000) as! UILabel
        label.text = item.text
    }
    
    func configureCheck(for cell: UITableViewCell, with item: ChecklistItem) {
        cell.accessoryType = item.checked ? .checkmark : .none
    }
    
}

// MARK: - Tableview Datasource
// TODO: Datasource는 데이터를 가지고 테이블뷰의 행을 관리하는 Delegate
extension ChecklistViewController {
    // 섹션에서 보여줄 총 행의 갯수
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    // 각 행에 보여줄 데이터를 셀을통해 보여줌
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // ChecklistItem 식별자를 가진 셀을 재사용
        let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
        let item = items[indexPath.row]
        configureText(for: cell, with: item)
        configureCheck(for: cell, with: item)
        return cell
    }
}

// MARK: - Tableview Delegate
// TODO: 터치, 편집 같은 테이블 뷰 고유 기능의 처리를 관리하는 델리게이트
extension ChecklistViewController {
    override func tableView(_ tableView: UITableView,
                            didSelectRowAt indexPath: IndexPath) {
        
        if let cell = tableView.cellForRow(at: indexPath) {
            let item = items[indexPath.row]
            item.checked.toggle()
            configureCheck(for: cell, with: item)
        }
        
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

/*
 Issue1 : 선택을 해제하고 행을 올리다보면 다른행에서 해당 셀을 재사용하기 때문에 체크가 해제되거나 다시 선택되는 경우가 있음
 -> 체크 표시를 표시할지 여부를 기억하기 위해 셀의 액세서리를 사용하는 대신 각 행의 확인 상태를 추적하는 방법이 필요합니다. 즉, 데이터 소스를 확장하고 다음 섹션의 주제인 적절한 데이터 모델을 사용하도록 해야 합니다.
 */

댓글남기기