테이블 뷰의 재사용 매커니즘은 iOS 특유의 부드러운 화면을 위해 사용되는 몇가지 매커니즘 중 하나다.

테이블 뷰가 데이터 소스의 양만큼 셀을 생성하면 화면 스크롤 시 움직여야 할 셀의 수와, 이에대한 처리를 하기 위해 사용되는 메모리도 많아지므로 부드러운 스크롤을 기대하기 힘들다.

하지만 재사용 매커니즘 덕분에 셀의 수가 많아도 그와 관계없이 부드러운 흐름의 UI를 만들 수 있다.

재사용 매커니즘의 동작원리 ( dequeseReusableCell ..)

  1. 테이블 뷰가 화면에 나타낼 셀 객체를 자신의 데이터 소스한테 요청
  2. 데이터 소스는 테이블 뷰의 재사용 큐에서 사용 가능한 셀이 있는지 확인하여 만일 있으면 그중 하나를 꺼내 전달하고, 없으면 새로운 셀을 생성
  3. cellForRowAt 메소드가 셀을 구성한 다음 반환하면 테이블 뷰는 이 셀을 받아 화면에 표시
  4. 사용자가 테이블 뷰를 스크롤함에 따라 화면을 벗어난 셀은 테이블 뷰에서 제거되지만 완전히 삭세하는게 아니라 재사용 큐에 추가함.
  5. 사용자의 스크롤에 따라1~4 과정 반복

여기서 주의할점은, 재사용 큐에 저장된 셀 자체는 재사용하지만, 셀의 콘텐츠는 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
    }
    
 }