이번 포스팅은 프로젝트를 진행하던 도중에 생긴 궁금증도 해결할 겸 작성해 본다.
프로젝트는 간단한 날씨 앱 프로젝트 인데, TableView
의 HeaderView를 생성해야 할 일이 있었다.
상황은 아래의 그림과 같이 현재 날씨를 받아와서 보여주는 부분과
5일치의 예보 날씨를 보여주는 부분이 나뉘게 된다.
여기서 TableView
를 사용해서 구현하는 방법에는 2가지 있다고 생각한다.
TableViewCell
과 예보날씨를 나타내는 TableViewCell
각각 생성TableViewCell
생성과 TableViewHeader
에 현재 날씨 구현이 두 가지 방법 중에서 2번째 방법으로 구현을 해보려고 한다.
2번째 방법을 선택한 이유는 만약 현재 날씨를 나타내는 cell
이 여러 개이고 반복 되는 것이라면 1번 방법처럼 이를 하나의 CustomTableViewCell
로 구현해 2개의 Section으로 구현했을 것 같다.
그러나 위의 경우에는 현재 날씨 cell
은 한 번만 나타나기 때문에 TableViewHeader
에 구현해도 괜찮을 것 같다고 생각했다.
그렇다면 이를 구현하는 방법에 대해서 알아보자.
Table View에서 Section
은 Header + rows + Footer
를 의미한다.
이때 Header
, Footer
는 다른 section과의 구분을 지어주는 영역으로 section의 시작과 끝을 알 수 있는 부분이다.
처음에는 TableView
의 style을 default인 plain
으로 하여 TableViewHeaderView
를 추가하였다.
추가하는 방법은 다음과 같다.
UITableViewHeaderFooterView
객체 생성하고 Footer view를 구현 import UIKit
final class WeatherTableViewHeaderView: UITableViewHeaderFooterView {
static let headerViewID = "WeatherTableViewHeaderView"
var weatherImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private var addressLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private var mininumAndMaximumTemperatureLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
label.font = .preferredFont(forTextStyle: .body)
return label
}()
private var currentTemperatureLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
label.font = .preferredFont(forTextStyle: .largeTitle)
return label
}()
private var verticalStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.spacing = 10
return stackView
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setupHeaderView()
configureLayout()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func setupHeaderView() {
verticalStackView.addArrangedSubview(addressLabel)
verticalStackView.addArrangedSubview(mininumAndMaximumTemperatureLabel)
verticalStackView.addArrangedSubview(currentTemperatureLabel)
contentView.addSubview(weatherImageView)
contentView.addSubview(verticalStackView)
}
private func configureLayout() {
NSLayoutConstraint.activate([
weatherImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
weatherImageView.widthAnchor.constraint(equalToConstant: 100),
weatherImageView.heightAnchor.constraint(equalTo: weatherImageView.widthAnchor),
weatherImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
verticalStackView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 5),
verticalStackView.leadingAnchor.constraint(equalTo: weatherImageView.trailingAnchor, constant: 10),
verticalStackView.bottomAnchor.constraint(greaterThanOrEqualTo: contentView.bottomAnchor, constant: -5),
verticalStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
verticalStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10)
])
}
}
UITableViewHeaderFooterView
객체를 table view에 등록하기final class MainViewController: UIViewController {
private let weatherTableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupWeatherTableView()
}
private func setupWeatherTableView() {
weatherTableView.delegate = self
weatherTableView.dataSource = self
weatherTableView.register(WeatherTableViewHeaderView.self, forHeaderFooterViewReuseIdentifier: WeatherTableViewHeaderView.headerViewID)
weatherTableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(weatherTableView)
NSLayoutConstraint.activate([
weatherTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
weatherTableView.topAnchor.constraint(equalTo: view.topAnchor),
weatherTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
weatherTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
tableView(\_:viewForHeaderInSection:)
함수에 생성한 UITableViewHeaderFooterView
객체를 headerview로 불러와서 사용 func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let weatherTableViewHeaderView = tableView.dequeueReusableHeaderFooterView(withIdentifier: WeatherTableViewHeaderView.headerViewID) as? WeatherTableViewHeaderView else {
return UIView()
}
// weatherTableViewHeaderView property value configure
// ...
return weatherTableViewHeaderView
}
위와 같이 코드를 작성하고 실행한 결과 아래와 같이 동작하게 된다.
그러나 다음과 같이 TableView
를 스크롤 할 때 TableViewHeaderView
가 내려가는 것이 아니라 Section이 끝날 때까지 고정되어 있게 된다. 따라서 이 문제를 해결하기 위해서는 다음과 같이 수정해주었다.
먼저 해당 문제는 TableView
의 style이 plain
으로 되어 있기 때문이었다.
따라서 TableView
의 style을 grouped
로 변경해주었다.
그렇게 하면 정상적으로 TableViewHeaderView
가 고정되지 않고 스크롤 되는 것을 확인해 볼 수 있다.
// 기존 코드
import UIKit
final class MainViewController: UIViewController {
private let weatherTableView = UITableView()
// ...
}
// 수정 코드
import UIKit
final class MainViewController: UIViewController {
private let weatherTableView = UITableView(frame: CGRect.zero, style: .grouped)
//...
}
위와 같이 코드를 수정하고 실행한 결과 아래와 같이 동작하게 된다.
이제 정상적으로 TableView
를 스크롤 할 때 TableViewHeaderView
가 고정되지 않고 스크롤 되는 것을 확인해 볼 수 있다.
table view
는 아래와 같은 3가지 style을 가진다.
plain
: 기존 table view style이다.grouped
: plain에서 header와 footer의 간격을 늘려주는것inset grouped
: rows들의 bounds가 둥근 모형이며, row 양 옆에 inset이 들어가는 것TableView
의 style을 plain → grouped로 변경하고 사용하게되면 문제점이 발생할 수 있다.
만약 Footer가 필요없고 Header만 필요한 경우에도 자동적으로 Footer까지 생성되게 된다.
(반대로 Header가 필요없고 Footer가 필요한 경우도 발생 가능)
이러한 경우에는 다음과 같이 해결할 수 있다.
let tableView = UITableView()
// footer가 필요없는 경우
tableView.sectionFooterHeight = 0
// header가 필요없는 경우
tableView.sectionHeaderHeight = 0