테이블 뷰의 재사용 매커니즘
은 iOS 특유의 부드러운 화면을 위해 사용되는 몇가지 매커니즘 중 하나다.
테이블 뷰가 데이터 소스의 양만큼 셀을 생성하면 화면 스크롤 시 움직여야 할 셀의 수와, 이에대한 처리를 하기 위해 사용되는 메모리도 많아지므로 부드러운 스크롤을 기대하기 힘들다.
하지만 재사용 매커니즘 덕분에 셀의 수가 많아도 그와 관계없이 부드러운 흐름의 UI를 만들 수 있다.
여기서 주의할점은, 재사용 큐에 저장된 셀 자체는 재사용하지만, 셀의 콘텐츠는 cellForRowAt 메소드를 통하여 매번 새롭게 구성한다는 것이다. 다시말해 화면에 새로운 셀을 표시할 때다마 cellForRowAt 메소드는 매번 다시 실행된다. 이미 만들어져 화면에 노출된 셀이라도 일단 화면을 벗어나 테이블 뷰에서 제거되고 나면 이 셀을 다시 화면에 표시하기 위해서는 해당 메소드를 거쳐야 한다.
⇒ 그래서 썸네일 이미지를 읽어오는 코드를 구현했을때 cellForRowAt메소드가 실행되면서 화면의 스크롤이 버벅거렸다.!
⇒ 이러한 문제를 피하려면 iOS 개발에 관하여 다음 원칙들을 적용해야한다.
→ 반복적으로 호출되는 메소드의 내부에는 네트워크 통신 등 처리 시간이 긴 로직을 포함하지 않아야 한다.
→ 네트워크 통신을 통해 읽어온 데이터는 재사용 할 수 있도록 캐싱 처리하여 통신횟수를 줄여야 한다.
→ 네트워크 통신이나 시간이 오래 걸리는 코드를 사용할 때는 비동기로 처리한다.
그럼 9강에서 했던 실습 코드를 수정해보자.
이미지를 읽어오는 코드를 cellForRowAt가 아닌 callMovieAPI()에서 데이터를 읽어온 다음으로 옮긴다.
//
// MovieVO.swift
// MyMovieChart
//
// Created by 김희진 on 2022/04/26.
//
import Foundation
import UIKit
class MovieVO {
var thumbnail: String?
var title: String?
var description: String?
var detail: String?
var opendate: String?
var rating: Double?
var thumbnailImage: UIImage?
}
//
// ListViewController.swift
// MyMovieChart
//
// Created by 김희진 on 2022/04/26.
//
import UIKit
class ListViewController: UITableViewController {
var dataset = [("다크나이트", "영웅물에 철학에 음악까기 더해져 예술이 된다.", "2008-09-04", 9.41, "1.png"),
("다크나이트", "영웅물에 철학에 음악까기 더해져 예술이 된다.", "2008-09-05", 9.41, "2.png"),
("다크나이트", "영웅물에 철학에 음악까기 더해져 예술이 된다.", "2008-09-06", 9.41, "3.png")]
var page = 1
lazy var list: [MovieVO] = {
var dataList = [MovieVO]()
for (title, desc, opendate, rating, thumbnail) in self.dataset {
var mvo = MovieVO()
mvo.title = title
mvo.description = desc
mvo.opendate = opendate
mvo.rating = rating
mvo.thumbnail = thumbnail
dataList.append(mvo)
}
return dataList
}()
@IBOutlet var moreButton: UIButton!
override func viewDidLoad() {
callMovieAPI()
//스태틱 셀일때만 쓰인다.
// tableView.register(MovieCell.self, forCellReuseIdentifier: "MovieCell")
// if #available(iOS 15.0, *) {
// tableView.sectionHeaderTopPadding = 0.0
// } else {
// // Fallback on earlier versions
// }
// var mvo = MovieVO()
// mvo.title = "다크나이트"
// mvo.description = "영웅물에 철학에 음악까지 더해져 예술이 되다."
// mvo.opendate = "2009-09-04"
// mvo.rating = 9.89
// self.list.append(mvo)
//
//
// mvo = MovieVO()
// mvo.title = "호무시절"
// mvo.description = "때를 알고 내리는 좋은 비"
// mvo.opendate = "2009-07-14"
// mvo.rating = 7.91
// self.list.append(mvo)
//
//
// mvo = MovieVO()
// mvo.title = "말할 수 없는 비밀"
// mvo.description = "여기서 너까지 다섯 걸음"
// mvo.opendate = "2015-10-31"
// mvo.rating = 9.19
// self.list.append(mvo)
}
func callMovieAPI(){
let url = "<http://swiftapi.rubypaper.co.kr:2029/hoppin/movies?version=1&page=\\(self.page)&count=30&genreId=&order=releasedateasc>"
let apiURI: URL! = URL(string: url)
let apiData = try! Data(contentsOf: apiURI)
// apiData에 저장된 데이터를 출력하기 위해 NSString타입의 문자열로 변환해야함.
// 두번째 파라미터 encoding이 인코딩 형식이다.
let log = NSString(data: apiData, encoding: String.Encoding.utf8.rawValue) ?? ""
NSLog("API Result=\\(log)")
do{
// api호출결과는 Data 타입이여서 로그를 출력하기 위해 NSString 으로 변환하였듯이, 테이블을 구성하는 데이터로 사용하려면 NSDictionary 객체로 변환해야한다.
// NSDictionary 객체는 키-값 쌍으로 되어있어 JSONObject와 호환이 된다.
// 만약 데이터가 리스트 형해로 전달되었다면 NSArray객체를 이용해야한다.
// 파싱할 떄는 jsonObject를 이용한다. 근데 jsonObject는 파싱 과정에서 오류가 발생하면 이를 예외로 던지게 설계되어 있어 do~try~catch 구문으로 감싸줘야한다.
// jsonObject실행 결과로 NSDictionary, NSArray 형태가 나온다. 양쪽을 모두 지원하기 위해 jsonObject 은 Any를 리턴하므로 이 결과값을 원하는 값으로 캐스팅해서 받아야한다.
let apiDictionary = try JSONSerialization.jsonObject(with: apiData, options: []) as! NSDictionary
let hoppin = apiDictionary["hoppin"] as! NSDictionary
let totalCount = (hoppin["totalCount"] as? NSString)!.integerValue
let movies = hoppin["movies"] as! NSDictionary
let movie = movies["movie"] as! NSArray
for row in movie {
let r = row as! NSDictionary
let mvo = MovieVO()
mvo.title = r["title"] as? String
mvo.description = r["genreNames"] as? String
mvo.thumbnail = r["thumbnailImage"] as? String
mvo.detail = r["linkUrl"] as? String
mvo.rating = ((r["ratingAverage"] as! NSString).doubleValue)
let url: URL! = URL(string: mvo.thumbnail!)
let imageData = try! Data(contentsOf: url)
mvo.thumbnailImage = UIImage(data: imageData)
list.append(mvo)
}
if list.count > totalCount {
self.moreButton.isHidden = true
}
}catch{
NSLog("Parse Error!!")
}
}
@IBAction func more(_ sender: Any) {
self.page += 1
callMovieAPI()
self.tableView.reloadData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//셀에 들어갈 데이터
let row = list[indexPath.row]
//재사용 큐를 이용해 테1이블 뷰 셀 인스턴스 생성
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as? MovieCell else {
fatalError("not found")
}
cell.title.text = row.title
cell.desc.text = row.description
cell.opendate.text = row.opendate
cell.rate.text = "\\(row.rating!)"
if indexPath.row < 3 {
cell.thumbnail.image = UIImage(named: row.thumbnail!)
} else {
cell.thumbnail.image = row.thumbnailImage
}
// 커스텀 셀을 태그를 이용해서 사용. 따로 셀 클래스를 만들지 않고 identifier로 구분했다.
// let title = cell.viewWithTag(101) as? UILabel
// let desc = cell.viewWithTag(102) as? UILabel
// let opendate = cell.viewWithTag(103) as? UILabel
// let rating = cell.viewWithTag(104) as? UILabel
//
// title?.text = row.title
// desc?.text = row.description
// opendate?.text = row.opendate
// rating?.text = "\\(row.rating!)"
// 스태틱 셀
// cell.textLabel?.text = row.title
// cell.detailTextLabel?.text = row.description
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
NSLog("선택된 행은 \\(indexPath.row) 번째 행입니다.")
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}