[iOS] UITableView
설정
- Device Orientation : Portrait
- ViewController → ChecklistViewController ChecklistViewController: UITableViewController
- 스토리보드 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
- None
- Checkmark
- cell의 accessoryType 프로퍼티에 .checkmark를 하면 체크, .none 으로 할당하면 체크가 없어짐
cell.accessoryType = item.checked ? .checkmark : .none
- cell의 accessoryType 프로퍼티에 .checkmark를 하면 체크, .none 으로 할당하면 체크가 없어짐
- Detail
셀을 생성하는 4가지 방법
- 다이나믹 프로토타입 셀 스토리 보드에서 바로 셀을 구성하여 사용 간단하고 빠름
- 정적 셀 (static cell) 어떤 셀을 가질지 미리 알고 있음 DataSource 메소드를 제공할 필요가 없음 (ex : cellForRowAt)
- Nib file UITableViewCell 오브젝트만 포함하는 스토리보드와 비슷함 스토리보드 외부에서 사용한다는 점을 제외하면 다이나믹 프로토타입 셀을 사용하는 것과 유사함
- 하드 코딩 특정 셀 스타일을 지정할 수 있음 미리 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
- 사용자 정의 nib 파일로 UI를 구성 후 클래스를 구현
// Custom 헤더뷰를 만들기 위한 클래스
class LibraryHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier = "\(LibraryHeaderView.self)"
@IBOutlet var titleLabel: UILabel!
}
- 헤더뷰를 재사용하기 위해 reuseIdentifier를 static 프로퍼티로 정의 타이틀을 설정하기 위한 titleLabel 선언, nib파일에서 Outlet 연결
tableView.register(UINib(nibName: "\(LibraryHeaderView.self)", bundle: nil),
forHeaderFooterViewReuseIdentifier: LibraryHeaderView.reuseIdentifier)
- 헤더뷰를 재사용하기 위해 테이블 뷰에 등록.
tableView.register(UINib(nibName: "\(LibraryHeaderView.self)", bundle: nil),
forHeaderFooterViewReuseIdentifier: LibraryHeaderView.reuseIdentifier)
- 해당 섹션에 맞는 헤더뷰를 재사용
/// 테이블 뷰 섹션의 해더뷰에 대한 데이터 소스 요청
/// 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 : 선택을 해제하고 행을 올리다보면 다른행에서 해당 셀을 재사용하기 때문에 체크가 해제되거나 다시 선택되는 경우가 있음
-> 체크 표시를 표시할지 여부를 기억하기 위해 셀의 액세서리를 사용하는 대신 각 행의 확인 상태를 추적하는 방법이 필요합니다. 즉, 데이터 소스를 확장하고 다음 섹션의 주제인 적절한 데이터 모델을 사용하도록 해야 합니다.
*/
댓글남기기