Creating Expandable TableView Cells (Collapsable) - Xcode 12, Swift 5, iOS Development 2022
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.row == 0 {
sections[indexPath.section].isOpen.toggle()
tableView.reloadSections([indexPath.section], with: .automatic)
} else {
print("\(sections[indexPath.section].section.contents[indexPath.row - 1].name) is selected")
}
}
isOpen
값 여부를 통해 컨텐츠 뷰 셀을 보여줄 지 결정func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionPair = sections[section]
if sectionPair.isOpen {
return sectionPair.section.contents.count + 1
} else {
return 1
}
}
isOpen
값에 따라 섹션 셀만을 보여줄지, 섹션 셀 개수 + 각 섹션의 컨텐츠 뷰를 모두 보여줄 지 결정func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let sectionPair = sections[indexPath.section]
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
cell.textLabel?.text = sectionPair.section.title
return cell
} else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ExpandableTableViewCell.identifier, for: indexPath) as? ExpandableTableViewCell else { fatalError() }
let model = sectionPair.section.contents[indexPath.row - 1]
cell.configure(with: model)
return cell
}
}
import Foundation
struct PokemonModel {
let name: String
let imageURLString: String
}
import Foundation
enum PokemonSection: CaseIterable {
case fire
case electric
case water
case grass
var title: String {
switch self {
case .fire: return "Fire 🔥"
case .electric: return "Electric ⚡️"
case .water: return "Water 💧"
case .grass: return "Grass 🌿"
}
}
var contents: [PokemonModel] {
switch self {
case .electric:
return [
.init(name: "피카츄", imageURLString: "https://external-preview.redd.it/nBDdAwKA-k5APbu2bhP-1o1Yp9T0uz2moXOH_NFJqXQ.jpg?auto=webp&s=52a81b709960e3ecc902862fdf3a6c70ae68e9bd"),
.init(name: "라이츄", imageURLString: "https://static.wikia.nocookie.net/pokemon/images/4/49/Tierno_Raichu.png/revision/latest?cb=20150331013126")
]
case .fire:
return [
.init(name: "파이리", imageURLString: "https://mblogthumb-phinf.pstatic.net/MjAxNzA0MjFfMTkx/MDAxNDkyNzU0NDI5OTI0.mj6eO3WJYc_kzucy4qEHPIEAeWNyjxt7yRmKofp04twg.jXhG9fB3fjGcayrD5HYRoGUAWw0UF7-0O0B6-44VuSUg.PNG.azimong71/07.PNG?type=w2"),
.init(name: "리자드", imageURLString: "https://mblogthumb-phinf.pstatic.net/20120619_252/ahn3607_1340035118014tH6qc_JPEG/Pokemon046_-_%BA%CE%C8%B0%A3%BF_%C8%AD%BC%AE_%C6%F7%C4%CF%B8%F3.avi_000955133.jpg?type=w2"),
.init(name: "파이리", imageURLString: "https://t1.daumcdn.net/cfile/tistory/99BB433359E8C2BF32"),
.init(name: "파이리", imageURLString: "https://t1.daumcdn.net/cfile/tistory/9984553359E8C2C21D"),
.init(name: "파이리", imageURLString: "https://t1.daumcdn.net/cfile/tistory/99F5223359E8C2C02B")
]
case .grass:
return [
.init(name: "이상해씨", imageURLString: "https://static0.gamerantimages.com/wordpress/wp-content/uploads/2021/12/pokemon-bulbasaur-smile-feature.jpg")
]
case .water:
return [
.init(name: "꼬부기", imageURLString: "https://t1.daumcdn.net/cfile/tistory/9906A93359E8C85107")
]
}
}
}
import UIKit
class ExpandableTableViewCell: UITableViewCell {
static let identifier = "ExpandableTableViewCell"
private let pokemonImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
return imageView
}()
private let pokemonNameLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textAlignment = .center
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
pokemonImageView.image = nil
}
override func layoutSubviews() {
super.layoutSubviews()
let size = contentView.frame.size.height
pokemonImageView.frame = CGRect(x: contentView.safeAreaInsets.left + 40, y: 0, width: size, height: size)
pokemonImageView.layer.cornerRadius = size / 2
pokemonNameLabel.frame = CGRect(x: contentView.safeAreaInsets.left + size + 50, y: 0, width: contentView.frame.size.width - size - 10 - contentView.safeAreaInsets.left - 40, height: size)
}
private func setUI() {
contentView.addSubview(pokemonImageView)
contentView.addSubview(pokemonNameLabel)
}
func configure(with model: PokemonModel) {
pokemonNameLabel.text = model.name
guard let url = URL(string: model.imageURLString) else { return }
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 400,
error == nil else { return }
DispatchQueue.main.async { [weak self] in
self?.pokemonImageView.image = UIImage(data: data)
}
}
.resume()
}
}
class ExpandableTableViewController: UIViewController {
typealias SectionPair = (section: PokemonSection, isOpen: Bool)
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.register(ExpandableTableViewCell.self, forCellReuseIdentifier: ExpandableTableViewCell.identifier)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
private var sections: [SectionPair] = PokemonSection.allCases.map({($0, false)})
override func viewDidLoad() {
super.viewDidLoad()
setUI()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
private func setUI() {
view.backgroundColor = .systemBackground
view.addSubview(tableView)
}
}
typeAlias
를 통해 각 섹션 및 토글 여부를 간단하게 표현extension ExpandableTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.row == 0 {
sections[indexPath.section].isOpen.toggle()
tableView.reloadSections([indexPath.section], with: .automatic)
} else {
print("\(sections[indexPath.section].section.contents[indexPath.row - 1].name) is selected")
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return view.frame.size.height / 16
} else {
return view.frame.size.height / 8
}
}
}
extension ExpandableTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionPair = sections[section]
if sectionPair.isOpen {
return sectionPair.section.contents.count + 1
} else {
return 1
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let sectionPair = sections[indexPath.section]
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
cell.textLabel?.text = sectionPair.section.title
return cell
} else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ExpandableTableViewCell.identifier, for: indexPath) as? ExpandableTableViewCell else { fatalError() }
let model = sectionPair.section.contents[indexPath.row - 1]
cell.configure(with: model)
return cell
}
}
}
reloadSections
를 통해 해당 섹션 셀만 렌더링