우선 재사용되는 셀은 UITableViewCell, UICollectionViewCell 등이 있습니다.
재사용 LifeCycle
이런 재사용 원리를 사용하여 메모리를 절약하고 성능을 향상하게 됩니다.
이 재사용 LifeCycle에는 LifeCycle Method가 있습니다.
어떤 순서로 호출되는지 확인해봅시다.
import UIKit
class ViewController: UIViewController {
var tableView: UITableView = UITableView()
let data = Array(1...20)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setTableView()
}
func setTableView() {
tableView.dataSource = self
tableView.prefetchDataSource = self
tableView.delegate = self
tableView.register(MyCell.self, forCellReuseIdentifier: MyCell.identifier)
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell: MyCell = tableView.dequeueReusableCell(withIdentifier: MyCell.identifier, for: indexPath) as? MyCell else { return UITableViewCell() }
cell.row = indexPath.row
cell.textLabel?.text = "Cell \(indexPath.row)"
print("cellForRowAt: \(indexPath.row)")
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print("willDisplay: \(indexPath.row)")
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print("didEndDisplaying: \(indexPath.row)")
}
}
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
print("prefetchRowsAt: \(indexPaths)")
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
print("cancelPrefetchingForRowsAt: \(indexPaths)")
}
}
import UIKit
class MyCell: UITableViewCell {
static let identifier: String = "MyCell"
var row: Int?
override func awakeFromNib() { // 스토리보드 기반, xib등 으로 셀을 다룰 경우에만 호출
super.awakeFromNib()
print("awakeFromNib, row:\(row ?? 0)")
}
override func prepareForReuse() {
print("prepareForReuse, row:\(row ?? 0)")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
print("init, row:\(row ?? 0)")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("deinit, row:\(row ?? 0)")
}
}
실행화면
init, row:0
cellForRowAt: 0
willDisplay: 0
init, row:0
cellForRowAt: 1
willDisplay: 1
init, row:0
cellForRowAt: 2
willDisplay: 2
init, row:0
cellForRowAt: 3
willDisplay: 3
...(중략)
init, row:0
cellForRowAt: 18
willDisplay: 18
prefetchRowsAt: [[0, 19], [0, 20], [0, 21], [0, 22], [0, 23], [0, 24], [0, 25], [0, 26], [0, 27], [0, 28]]
init, row:0
cellForRowAt: 19
init, row:0
cellForRowAt: 21
didEndDisplaying: 0
willDisplay: 21
init, row:0
cellForRowAt: 22
didEndDisplaying: 1
willDisplay: 22
prepareForReuse, row:0
cellForRowAt: 23
didEndDisplaying: 2
willDisplay: 23
prepareForReuse, row:1
cellForRowAt: 24
didEndDisplaying: 3
willDisplay: 24
prepareForReuse, row:2
cellForRowAt: 25
로그에 찍힌 메서드들부터 알아보겠습니다. 애플 도큐먼트 참고했습니다.
이렇게 총 첫화면에서 보이는 19개 셀이 로드 되며 init을 호출, cellForRowAt에 의해 셀들이 메모리에 로드, willDisplay로 화면에 보이기 이전 호출이 반복되게 됩니다.
그리고 살짝 테이블뷰를 드래그 하기 시작하면
사용자가 테이블을 계속 드래그 해서 이제 셀이 화면 바깥으로 사라지기 시작하면?
그리고 didEndDisplaying이 호출되는 이후 특정 시점부터는 init 메서드가 호출되지 않습니다. 대신...
즉, 재사용된 셀을 사용할 때는 prepareForReuse 메서드만 호출되게 되는 것입니다.
생명 주기 메서드를 활용해 문제를 해결해봅시다.
유저 정보를 랜덤으로 보내주는 API입니다. 이걸 이용해서 동적으로 셀에 데이터를 추가해볼게요
한번 이렇게 만들어봅시다. 드래그해서 셀이 화면에 나타날때마다 해당 셀의 이미지가 로딩됩니다.
약간만 코드를 수정해봅시다. Alamofire를 사용해 서버로부터 데이터를 가져옵니다.
import UIKit
import Alamofire
class ViewController: UIViewController {
var tableView: UITableView = UITableView()
let data = Array(1...20)
var userData: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setTableView()
}
override func viewDidAppear(_ animated: Bool) {
requestUserData()
}
func setTableView() {
tableView.dataSource = self
tableView.prefetchDataSource = self
tableView.delegate = self
tableView.register(MyCell.self, forCellReuseIdentifier: MyCell.identifier)
view.addSubview(tableView)
tableView.rowHeight = 100
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
}
func requestUserData() {
guard let url: URL = URL(string: "https://randomuser.me/api/?results=20&inc=name,email,picture") else { return }
AF.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: ["Content-Type":"application/json", "Accept":"application/json"], interceptor: nil, requestModifier: nil)
.validate()
.responseJSON { response in
switch response.result {
case .success:
guard let data = response.data else { return }
do {
let jsonData = try JSONDecoder().decode(APIResponse.self, from: data)
self.userData = jsonData.results
self.tableView.reloadData()
} catch let err {
print(err.localizedDescription)
}
case .failure(let err):
print(err.localizedDescription)
}
}
.resume()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellForRowAt: \(indexPath.row)")
guard let cell: MyCell = tableView.dequeueReusableCell(withIdentifier: MyCell.identifier, for: indexPath) as? MyCell else { return UITableViewCell() }
DispatchQueue.global().async {
guard let thumbnailUrl = URL(string: self.userData[indexPath.row].picture.thumbnail) else { return }
guard let imageData = try? Data(contentsOf: thumbnailUrl) else { return }
DispatchQueue.main.async {
cell.profileImageView.image = UIImage(data: imageData)
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
}
cell.row = indexPath.row
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print("willDisplay: \(indexPath.row)")
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print("didEndDisplaying: \(indexPath.row)")
}
}
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
print("prefetchRowsAt: \(indexPaths)")
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
print("cancelPrefetchingForRowsAt: \(indexPaths)")
}
}
import UIKit
class MyCell: UITableViewCell {
static let identifier: String = "MyCell"
var row: Int?
var profileImageView: UIImageView = UIImageView()
override func awakeFromNib() {
super.awakeFromNib()
print("awakeFromNib, row:\(row ?? 0)")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
print("init, row:\(row ?? 0)")
layoutCellProperties()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
print("prepareForReuse, row:\(row ?? 0)")
// 아래 부분 주석은 나중에 해제해 줍시다.
// profileImageView.image = nil
}
deinit {
print("deinit, row:\(row ?? 0)")
}
func layoutCellProperties() {
profileImageView.contentMode = .scaleToFill
contentView.addSubview(profileImageView)
profileImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
profileImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.2),
profileImageView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.9),
profileImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
profileImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8)
])
}
}
import Foundation
struct APIResponse: Codable {
let results: [User]
}
struct User: Codable {
struct Name: Codable {
let title: String
let first: String
let last: String
var full: String {
return self.title.capitalized + ". " + self.first.capitalized + " " + self.last.capitalized
}
}
struct Picture: Codable {
let large: String
let medium: String
let thumbnail: String
}
let name: Name
let email: String
let picture: Picture
}
주목할 점은 MyCell.swift에 있는 prepareForReuse 생명주기 메서드입니다.
위 아래 셀들이 재사용되며 이전 이미지가 아주 잠깐이지만 나타나는 것을 볼 수 있습니다.
왜냐하면 재사용 큐에 들어간 셀의 이전 이미지뷰를 초기화 해주는 부분이 없기 때문입니다.
이러면 사용자 입장에서 보기 좋지 않을 것 같아요.