티스토리 뷰

더이상 같은 프로젝트를 가지고 포스팅을 이어나가는게 무리라는 판단이 섰다. 일단 프로젝트 자체도 뭔가 완벽히 기획된 상태가 아니고, 어떤 기능을 배우기 위해서 컨텐츠를 추가하면 그 컨텐츠에 필요한 부가적인 코드를 작성해야 하기 때문에 논점이 흐려진다. 대신 프로젝트는 따로 진행하고 진행하면서 배우게 된 큰 단위의 개념들을 모아서 따로 예제를 만들면서 포스팅하는 방향으로 바꿨다.

 

 

 

UITableView

UITableView는 스크롤 할 수 있는 row(행)집합을 그린다. 위아래로 스크롤 할 수 있으며, 기본적으로 .plain, .grouped라는 2가지 스타일을 제공한다. plain은 row에 채울 데이터 값이 없어도 table을 그리지만 insetGrouped와 grouped의 경우 필요한 row만 그리고 나머지는 공백으로 그린다.

 

기본 설정은 plain이다.

 

UITableViewController를 상속받아서 Controller를 만든다면 UITableViewDelegate와, UITableViewDataSource를 따로 extension해서 구현할 필요가 없지만 그냥 UIViewController를 상속받은 ViewController에 tableView를 추가해서 사용한다면 delegate와 dataSource를 확장해서 내용을 작성해 주어야 한다.

특이한 점은 다른 view들과 다르게 생성할 때만 적용할 수 있는 style이라는 값을 가지고 있기 때문에 생성자로 style을 설정해 주어야 한다. 이 style은 get-only property로 값을 얻을 수는 있지만 변경할 수 없다.

 

// MyTableViewController.swift
override func viewDidLoad() {
	super.viewDidLoad()

	let tableView = UITableView(frame: self.view.frame, style: .plain)
	tableView.delegate = self
	tableView.dataSource = self

	view.addSubview(menuTable)
}

 

delegate와 dataSource를 self로 설정해 주었기 때문에 protocol을 따라 함수를 작성해야 한다. 예제는 기본 UIViewController를 상속받은 Controller를 사용하고 있기 때문에 따로 extension을 작성한다.

지금까지도 많이 나온 키워드지만 다시 짚고 넘어자면 delegate는 앞에 붙은 view에서 발생하는 이벤트를 처리하고, dataSource는 앞에 붙은 view를 만들어내기 위해서 필요한 데이터를 요구한다.

 

// MyTableViewController.swift
extension class MyTableViewController: UITableViewDelegate, UITableViewDataSource {
	// MARK: DataSource
	func numberOfSection(in tableView: UITableView) -> Int {
		return 1
	}

	func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return 2
	}

	func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)

		// configure cell

		return cell
	}

	// MARK: Delegate
	func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

	}
}

 

먼저 protocol에서 반드시 필요한 세가지를 정의했고, Delegate와 차이점을 알아보기 위해서 delegate 이벤트도 하나 정의했다. 쉽게 볼 수 있는 차이점은 DataSource protocol의 경우 반환값을 물어보는 질의문 형태라면 Delegate protocol은 반환값이 없고 didSelectRowAt과 같은 직관적인 이름으로 어떤 이벤트가 발생했을 때 호출되는 함수인지 알 수 있다.

 

 

 

UITableViewCell

위 코드에서 cell을 요구하는 DataSource protocol 함수의 반환값이 UITableViewCell이다. 이 cell이 바로 table의 각 row를 구성하는 view를 말한다. 그리고 이 cell을 코드로 만들어 볼 것이다.

새로운 파일을 만들고 UITableViewCell을 상속받는다. cell은 자신의 이름을 출력할 수 있는 label이 있으면 좋겠고, 선택되면 보라색으로 하이라이트 되면 좋겠다.

 

// MyTableViewCell.swift
class MyTableViewCell: UITableViewCell {
	override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
		super.init(style: style, reuseIdentifier: reuseIdentifier)

		self.selectionStyle = .default
		self.selectedBackgroundView = UIView(frame: contentView.frame)
		self.selectedBackgroundView?.backgroundColor = .systemPurple

		let titleLabel = UILabel()
		titleLabel.font = .systemFont(ofSize: 16)
		contentView.addSubView(titleLabel)
	}
}

 

UITableViewCell은 reuseIdentifier라는 String과 style로 생성할 수 있는데, style은 기본 .default로 생성되고, reuseIdentifier는 register할 때 입력한 값으로 생성되거나, storyboard, XIB 파일로 생성할 경우 GUI에서 입력한 값으로 생성된다.

예제는 코드로 모든 작업을 하는 것을 지향하기 때문에 만든 cell을 tableView에 register시키는 코드를 추가한다. 앞서 DataSource protocol의 cellForRowAt에서 사용했던 reuseIdentifier가 바로 register에 사용된 상수 문자열이다. xcode에서 UITableViewController를 만들면 이런식으로 reuseIdentifier를 정의해서 사용하지만 필요에 따라 다른 방법을 사용할 수도 있다.

 

// MyTableViewController.swift
private let reuseIdentifier: String = "Cell"

// ...

tableView.register(MyTableViewCell.self, forCellReuseIdentifier: reuseIdentifier) 

 

위에서 numberOfRowsInSection값을 그냥 상수로 반환했는데, 이제 cell도 만들었으니 data에 따라서 다양한 모습을 만들 수 있도록 수정해보자.

 

 

 

Data

MyTableViewCell에서 data를 사용할 수 있는 부분으로 title이 있다. 직관적으로 색 이름을 배열에 저장하고 data로 사용한다. DataSource protocol도 수정해서 data에 따라 cell의 개수가 변할 수 있도록 한다.

 

// MyTableViewController.swift
class MyTableViewController: UIViewController {
	let cellTitles: [String] = ["Red", "Green", "Blue", "Black"]
	// ...
}

extension MyTableViewController: UITableViewDelegate, UITableViewDataSource {
	// MARK: DataSource
	func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return self.cellTitles.count
	}

	func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		let cell = tableView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
	
		if let myCell = cell as? MyTableViewCell, indexPath.row < cellTitles.count {
			myCell.titleLabel.text = cellTitles[indexPath.row]
			return myCell
		}

		return cell
	}
}

 

아마 이렇게 까지 작성하고 실행해도 기본 rowHeight값이 44로 설정되기 때문에 화면에 table이 그려질 것이다. 4개의 cell에 title이 찍혀서 나오면 성공이다.

 

constraint를 지정하지 않았기 때문에 이상하게 나올 수도 있지만 이제 그 작업을 할거라서 상관없다.

 

 

 

Dynamic row height

앞서 말한 것 처럼 기본 rowHeight가 44로 설정되기 때문에 글 한 줄 정도 들어가는 cell로 구성한다면 큰 문제는 없다. 하지만 긴 글이나 사진 등 높이가 다양한 내용을 출력해야 한다면 고정된 높이로는 힘들다. 그래서 UITableView에는 cell을 구성하는 내용의 크기에 따라 유동적인 rowHeight를 사용할 수 있도록 automaticDimension이라는 상수가 정의되어 있다.

 

// MyTableViewController.swift
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44

 

estimatedRowHeight는 예상값으로 기본 44를 설정했다. 이제 cell의 절대적인 크기를 정해줘야 한다. TableViewCell로 이동해서 특히 높이를 정확히 계산할 수 있도록 constraint를 적절히 설정해보자.

 

// MyTableViewCell.swift
contentView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10).isActive = true
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10).isActive = true

 

contentView는 cell의 view hierarchy 최상위에 있는 view로 이 view의 크기를 직접 계산할 수 있어야만 automaticDimension이 제대로 동작한다. label의 경우 text 내용에 따라 width, height가 고정되기 때문에 top, leading, trailing, bottom을 contentView에 상대적으로 적용해서 결과적으로 contentView의 크기를 고정시킬 수 있다.

 

내용에 따라 달라지는 크기를 알아낼 수 있다.

 

 

 

Delegate

DataSource를 다뤄 보았으니 Delegate protocol도 하나쯤은 다뤄봐야 겠다. 위에서 작성했던 protocol에 로그를 출력하는 출력코드를 작성하고 실행해보자.

 

// MyTableViewController.swift
extension MyTableViewController: UITableViewDelegate, UITableViewDataSource {
	// MARK: Delegate
	func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
		guard indexPath.section == 0, indexPath.row < cellTitles.count else {
			print("unexpected indexPath")
			return
		}
		
		print("did select \(cellTitles[indexPath.row])")
	}
} 

 

 

 

Result

다른 기능을 제외하고 온전히 UITableView를 다루려고 하다보니 결국 로그를 찍는 결과를 내게 되었다.

 

흠.........

 

아 그리고 위 영상의 table을 보면 구분선 왼쪽이 비어있는 것을 볼 수 있는데, Apple이 UITableView를 만들면서 디자인 때문인지 separator의 left inset 값을 기본적으로 띄워놓고 있다. 이 값을 수정하면 공백을 없앨 수 있다.

 

// MyTableViewController.swift
tableView.separatorInset.left = 0
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함