[iOS] UITableViewDiffableDataSources

!·2023년 2월 23일
0

iOS

목록 보기
15/22

Overview

최근 UITableView를 학습하면서, 테이블 뷰 셀의 데이터들을 UISearchBar를 이용해서 검색할 때 reload() 메소드를 이용해 검색 조건에 맞는 데이터만 보여주는 식으로 처리했다. 이는 데이터가 많아질 수록 성능 저하로 나타날 수 있을뿐만 아니라, 사용자의 UI 경험에도 좋은 영향을 주지 못한다.

그 동안 UITableViewDataSource 공식문서를 볼 때마다 항상 지나쳤던 UITableViewDiffableDataSoruces가 눈에 띄었고 부랴부랴 WWDC 2019 에서 발표된 영상을 찾아보았고, 곧바로 매료되었다.
특히 기본적으로 제공해주는 애니메이션이, 검색 기능을 사용할때 매우 유용할 것 같다.


동작 방식

UITableViewDiffableDataSources는 snapshot을 의미한다.
즉, 현재의 데이터들의 상태를 기억해둔다는 뜻이다.

그림을 통해 살펴보자.

스냅샷은 크게 3가지를 위해 도입되었다.

  • 첫번째는 현재의 UI의 상태이다. 현재의 UI의 상태와 데이터의 변경이 이루어진 후의 UI로의 업데이트를 우아(?)하게 구현하기위함이다.
  • 두번째는 Section과 Item의 유일한 Identifier다. 여기서의 Item은 테이블 뷰의 셀에 해당한다. 매 UI 업데이트시마다 각 섹션과 셀을 구분하기 위해서는 이것들을 구분할 수 있는 방법이 필요하다. 따라서 UITableViewDiffableDataSources 는 몇가지 프로토콜을 준수하는 자료형에만 이를 사용할 수 있도록 하였다.
  • 세번째는 IndexPath를 사용할 필요가 없다. 왜냐하면, 이미 각각의 섹션과 로우가 몇가지 프로토콜을 따르게 함으로써 이들을 구분할 수 있기때문이다.

생각보다 쉽다 코드를 보자.


구현

UITableViewDataSources와 다르게 프로토콜이 아닌 클래스이다. 하나 하나 살펴보자

  • 먼저 SectionIdentifierType이다. 테이블 뷰의 섹션의 타입이며, where 조건절에 의해 Hashable 프로토콜을 따르는 타입이다. 즉, 각 섹션을 구분하기 위해서는 Section의 타입이 Hasable 프로토콜을 따라야한다.
enum Section: CaseIterable{
        case main
}

Swift의 원시 자료형(Int, String.. 등등)은 모두 Hashable 프로토콜을 따르기때문에 일단 넣으면 된다!
또한 열거형의 경우 원시값이 Hashable 프로토콜을 따르는 경우 열거형 자체도 Hashable 프로토콜을 따르기때문에 상관없다!

  • ItemIdentifier는 테이블 뷰로 따진다면 테이블 뷰 셀에 들어가는 데이터의 타입을 의미한다. 마찬가지로 Hashable 프로토콜을 따르도록 자료형을 구현해야한다.
var stringList: [String] = {
        var str = [String]()
        for x in 0...8{
            str.append("\(x)번째 셀 입니다.")
        }
        return str
    }()

String타입의 경우 Hashable 프로토콜을 따르므로 따로할 작업은 없다. 만약, 직접 데이터의 자료형을 구현해야한다면, Hashable 프로토콜을 따르도록 구현해야한다.

dataSource = UITableViewDiffableDataSource<Section, String>(tableView: tableView){
(tableView, indexPath, itemIdentifier) in
            
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
            cell.textLabel?.text = self.stringList[indexPath.row]
            cell.textLabel?.font = .systemFont(ofSize: 20)
            
            return cell
}

테이블 뷰의 dataSource에 DiffableDataSource 인스턴스를 넣는다. 최초의 테이블 뷰 로딩 및, UI 업데이트가 이루어진 후에 어떻게 테이블 뷰 셀이 구성되는지를 정의한다고 생각하면된다. 이렇게 구성된 셀의 모습으로 테이블 뷰 내의 셀들이 재배치 되는식이다.

스냅샷 인스턴스를 생성하기위한 구조체다. 제네릭 타입의 조건은 UITableViewDiffableDataSources와 완전히 동일하므로 그대로 사용하면 된다. 이번 포스트에서는 appendItem 메소드와, apply 메소드만 알아보겠다.

@IBAction func change(_ sender: Any) {
    
        // 빈 스냅샷 인스턴스 생성
        var snapShot = NSDiffableDataSourceSnapshot<Section, String>()
        
        // 스냅샷에 들어갈 테이뷰 뷰 섹션 추가.
        snapShot.appendSections([.main])
        
        var temp = [String]()
        temp = stringList.shuffled()
        
        // 스냅샷에 들어갈 셀 배열 추가.
        snapShot.appendItems(temp)
        
        // 스냅샷을 적용. UI 업데이트 시작
        dataSource.apply(snapShot, animatingDifferences: true)
        
    }

빈 스냅샷을 생성하면, 현재의 테이블 뷰의 모습이 찍히는 것이 아니다. 정말 테이블 뷰에 섹션과 셀이 없는 상태의 스냅샷 인스턴스가 생성된다. 이후, 내가 넣고 싶은 섹션과 셀을 위의 메소드에 인자로 전달한다.
이후, apply() 메소드를 호출하면 UI 업데이트가 진행된다.


전체 코드

import UIKit

class ListViewController: UITableViewController {

	// 셀에 담길 문자열
    var stringList: [String] = {
        var str = [String]()
        for x in 0...8{
            str.append("\(x)번째 셀 입니다.")
        }
        return str
    }()
    
    // 섹션을 열거형으로 정의
    enum Section: CaseIterable{
        case main
    }
    
    
    var dataSource: UITableViewDiffableDataSource<Section, String>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
   		
        
        dataSource = UITableViewDiffableDataSource<Section, String>(tableView: tableView)
        {(tableView, indexPath, itemIdentifier) in
            
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
            cell.textLabel?.text = self.stringList[indexPath.row]
            cell.textLabel?.font = .systemFont(ofSize: 20)
            
            return cell
        }
        
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return self.stringList.count
    }

    
    @IBAction func change(_ sender: Any) {
    
        // 빈 스냅샷 인스턴스 생성
        var snapShot = NSDiffableDataSourceSnapshot<Section, String>()
        
        // 스냅샷에 들어갈 테이뷰 뷰 섹션 추가.
        snapShot.appendSections([.main])
        
        var temp = [String]()
        temp = stringList.shuffled()
        
        // 스냅샷에 들어갈 셀 배열 추가.
        snapShot.appendItems(temp)
        
        // 스냅샷을 적용. UI 업데이트 시작
        dataSource.apply(snapShot, animatingDifferences: true)
        
    }
    
    
}
profile
개발자 지망생

0개의 댓글