테이블뷰 제네릭 CustomCombineDataSource

이전 포스팅에서 tableview데이터소스를 combine과 제네릭으로 연결을 했다.
데이터소스는 데이터와 연괸되어있고, 셀의 종류를 정하고, 리스트 개수가 몇개인지 정하는 역할이다 즉 데이터와 관련이 있다.

이전 포스팅에서 커스텀 DataSource를 만들어서 처리하였는데 문제가 있었다. 리스트를 보여줄 때 데이터 타입이 변경되면 받을 수 없고 DummyData타입만 받을 수 있었다. 어떠한 데이터가 오더라도 확장성이 있도록 호출하는 쪽에서 셀의 타입을 정하도록 구현해보자.

CustomCombineDataSource.swift

 //
 //  CustomCombineDataSource.swift
 //  UITableViewTutorial
 //
 //  Created by 김동현 on 5/15/25.
 //
 // https://www.youtube.com/watch?v=vlJ392OMkoI&list=PLgOlaPUIbynpuq9GKCwAedgWkkPm2Wo8v&index=16
 
 import UIKit
 
 /*
 class CustomCombineDataSource<T, G>: NSObject, UITableViewDataSource {
     
     // 멤버 변수
     var dataList: [T] = []
     
     var testDataList: [G] = []
 */
 
 class CustomCombineDataSource<Item>: NSObject, UITableViewDataSource {
     
     // 셀을 만드는 클로저
     // 1. let makeCell: (_ tableView: UITableView, _ indexPath: IndexPath, _ cellData: Item) -> UITableViewCell
     // 2. let makeCell: (UITableView, IndexPath, Item) -> UITableViewCell // 위랑 같음
     // 3.
     var makeCell: ((UITableView, IndexPath, Item) -> UITableViewCell)? = nil // 옵셔널로도 가능
     
     var dataList: [Item] = []
     
     // 2. 안쪽에서 터트리기 때문에 escaping 해주자
     /*
     init(makeCell: @escaping (_ tableView: UITableView, _ indexPath: IndexPath, _ cellData: Item) -> UITableViewCell) {
         self.makeCell = makeCell
         super.init()
     }
      */
     
     // 3. 옵셔널로 한다면 escaping 안해도 된다
     init(_ makeCell: ((_ tableView: UITableView, _ indexPath: IndexPath, _ cellData: Item) -> UITableViewCell)? = nil) {
         self.makeCell = makeCell
         super.init()
     }
     
     // MARK: - Combine 이벤트로 들어온 데이터랑 테이블뷰랑 연결시켜주는 지점
 
     /// 변경된 데이터를 받아서 테이블뷰에 적용한다
     /// - Parameters:
     ///   - updatedDataList: 외부에서 변경된 Combine Publisher로 들어온 데이터를 내 DataSource가 가진 data로 변경하기 위한 매개변수
     ///   - tableView: 리로드 대상 테이블뷰
     func pushDataList(_ updatedDataList: [Item], to tableView: UITableView) {
         tableView.dataSource = self
         self.dataList = updatedDataList
         tableView.reloadData()
     }
     
     // MARK: - 테이블뷰 데이터 소스 관련(변경이 된 데이터를 데이터소스로 넘겨받아서 reloadData를 하는 목적 
     
     /// 하나의 섹션에 몇개의 rows가 있냐
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         return dataList.count
     }
 
     /// 각 셀에 대한 내용을 구성하여 반환 -> 셀의 종류를 정하기 - 테이블뷰 셀을 만들어서 반환해라
     /// - indexPath: 셀의 위치를 나타내는 인덱스 경로
     /// - returns: 구성된 UITableViewCell 객체
     /// 어떤 셀을 보여줄지
     // 1. 테이블뷰
     // 2. indexPath 몇번째인지
     // 3. 셀에 대한 데이터 - 셀에 대한 제네릭 데이터
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         
         // 비어있으면 기본 UITableViewCell 반환
         makeCell?(tableView, indexPath, dataList[indexPath.row]) ?? UITableViewCell()
         
         //        /// 기본 스타일의 셀 생성 (textLabel과 detailTextLabel 포함)
         //        /// let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "MyCell")
         //
         //        // [guard let] 방식
         //        guard let cell = tableView.dequeueReusableCell(withIdentifier: CodeCell.reuseIdentifier, for: indexPath) as? CodeCell else {
         //            return UITableViewCell()
         //        }
         //
         //        if let dataList = dataList as? [DummyData] {
         //            let cellData: DummyData = dataList[indexPath.row]
         //
         //            /// 셀의 주 텍스트를 더미 데이터에서 가져오기
         //            cell.titleLabel.text = cellData.title
         //
         //            /// 셀의 서브 타이틀 설정
         //            cell.bodyLabel.text = cellData.body
         //
         //            cell.detailTextLabel?.numberOfLines = 0
         //        }
         //
         //        if let dataList = dataList as? [IndexData] {
         //            let cellData: IndexData = dataList[indexPath.row]
         //
         //            /// 셀의 주 텍스트를 더미 데이터에서 가져오기
         //            cell.titleLabel.text = cellData.title
         //
         //            /// 셀의 서브 타이틀 설정
         //            cell.bodyLabel.text = cellData.body
         //
         //            cell.detailTextLabel?.numberOfLines = 0
         //        }
         //
         //        return cell
     }
 }
 
 
 
 
 

tableView()의 각 매개변수 tableView, indexPath, 그리고 셀에 대한 데이터 cellData을 조합을 하여 UITableViewCell 즉 셀을 만들자. 기존 tableView() 내부를 전부 주석처리를 하고 주석부분을 만드는 클로저를 두자. 함수 = 클로저 = 논리이다. 즉 논리 = 로직이다. 다시말해 로직을 클로저로 빼자.

UITableView+Combine.swift

//
//  UITableView+Combine.swift
//  UITableViewTutorial
//
//  Created by 김동현 on 5/15/25.
//

import UIKit
import Combine

// MARK: - Sink 정의를 보면 매개변수로 (Self.Output) -> Void) 형태의 클로저를 받는다. 이 형태를 만족하는 로직 함수를 만들자.
// public func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
extension UITableView {
    // 고차함수 - 클로저를 매개변수로 가지거나 반환을 가지는 함수 자체
    // (Self.Output) -> Void)
    // ([DummyData]) -> Void
    // 데이터소스 바인딩    
    func customItemsWithCell<Item>(
        makeCell: ((UITableView, IndexPath, Item) -> UITableViewCell)? = nil
    ) -> ([Item]) -> Void {
        let dataSource = CustomCombineDataSource<Item>(makeCell)
        return { (updatedDateLisst: [Item]) in
            dataSource.pushDataList(updatedDateLisst, to: self) // 리로드
        }
    }
}

CombineListViewController.swift

//
//  CombineListViewController.swift
//  UITableViewTutorial
//
//  Created by 김동현 on 5/13/25.
//

import UIKit
import Combine

class CombineListViewController: UIViewController {
    
    // Combine 메모리 처리를 위해 생성
    var subscriptions = Set<AnyCancellable>()
    
    // Published를 하게 되면 dummies 데이터가 추가나 값 변경시 이벤트를 받을 수 있다.
    @Published var dummies: [DummyData] = []
    @Published var indexDatas: [IndexData] = []
    
    @IBOutlet weak var myTableView: UITableView!

   
    override func viewDidLoad() {
        super.viewDidLoad()
        configureTableView()
        
        
        /*
        - sink는 @Published가 수정된 스레드에서 실행된다
        - 그래서 Published변수 수정시 메인 스레드에서 수정해주자
        - @Published 값을 메인 스레드에서 수정하든, 백그라운드에서 수정하든, .receive(on: .main)만 붙이면 sink는 메인에서 실행되고reloadData()도 안전하게 실행된다
         */
        
        // MARK: - 기존의 데이터를 받는 거를 CombineListViewController에서 다했더라면 이제는 customDataSource으로 따로 뺴두고, 로직은 extension으로 빼서 처리를 한 것이다.
        // $ 붙이면 데이터 이벤트를 받을 수 있는 상태가 됨
        // sink는 구독하는 것이다.
        // AnyCancellable 구독한다고 한다.
        // store: 구독했던거에 대한 메모리 참조가 들어오게 되는데 이를 관리하기 위해 subscriptions에 넣어준다.
        /*
         기존 방식
         $dummies
             .receive(on: DispatchQueue.main)
             // 데이터 변경시마다 동작
             .sink(receiveValue: { (changedDummies: [DummyData]) in
                 print("changedDummies: \(changedDummies.count)")
                 
                 // sink는 메인스레드에서 동작해서 Dispatch안해도된다
                 self.myTableView.reloadData()
             })
             .store(in: &subscriptions)
         */
        
        $dummies.receive(on: DispatchQueue.main)
            .sink(receiveValue: self.myTableView.customItemsWithCell(
                // 셀에 대한 종류를 정해주기 위해 바깥으로 뺀 형태 -> makeCell을 데이터 타입마다 다르게 정의할 수 있다
                makeCell: { myTableView, indexPath, cellData in
                
                // [guard let] 방식
                guard let cell = myTableView.dequeueReusableCell(withIdentifier: CodeCell.reuseIdentifier, for: indexPath) as? CodeCell else {
                    return UITableViewCell()
                }
        
                cell.titleLabel.text = cellData.title  // 셀의 주 텍스트를 더미 데이터에서 가져오기
                cell.bodyLabel.text = cellData.body // 셀의 서브 타이틀 설정
                cell.detailTextLabel?.numberOfLines = 0
                return cell
                
            }))
            .store(in: &subscriptions)
        
        
        // 2초 뒤에 더미데이터 10개 추가
        DispatchQueue.global().asyncAfter(deadline: .now() + 2, execute: {
            self.dummies += DummyData.getDumies(10)
            // self.indexDatas += IndexData.getDumies(10)
        })
    }
    
    fileprivate func configureTableView() {
        
        // CodeCell에서는 이 줄만 필요
        self.myTableView.register(CodeCell.self, forCellReuseIdentifier: CodeCell.reuseIdentifier)
        // self.myTableView.delegate = self
    }
}

Leave a comment