관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. 추상화 - 제네릭스로 추상화하기 (#GenericTableView) 본문

iOS 응용 (swift)

[iOS - swift] 2. 추상화 - 제네릭스로 추상화하기 (#GenericTableView)

jake-kim 2024. 1. 31. 01:37

* 추상화하기 목차: https://ios-development.tistory.com/1627

제네릭스의 목표

  • 공통화, 추상화, 코드의 유연성

제네릭스 훑어보기 - 함수에 적용

  • 함수에 적용 - 함수 이름 오른쪽에 꺽쇠를 사용하여 타입 표현

before)

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}


func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

after)

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

제네릭스 훑어보기 - extension에 활용

ex) swift 내부에 정의된 Optional 타입

@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
	...
}

extension에서 제네릭스 parameter에 받는 경우)

extension Optional where Wrapped : Equatable {
    public static func != (lhs: Optional<Wrapped>, rhs: Optional<Wrapped>) -> Bool
}

extension에서 제네릭스와 함께 computed property 구현)

extension Optional where Wrapped == Bool {
    var isNilOrFalse: Bool {
        return !(self ?? false)
    }
}

제네릭스로 추상화하기

  • 제네릭스는 클래스, 구조체, 함수 등에 사용이 가능

ex) UITableView에 5개의 데이터가 있는 예제 코드

제네릭스를 안쓰는 경우 보통 아래처럼 구현) 

class CustomCell: UITableViewCell {
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        backgroundColor = .lightGray
        textLabel?.font = UIFont.boldSystemFont(ofSize: 18)
        textLabel?.textColor = .blue
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class ViewController: UIViewController {
    let tableView = UITableView()
    let data = [1, 2, 3, 4, 5]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.frame = view.bounds
        tableView.dataSource = self
        tableView.register(CustomCell.self, forCellReuseIdentifier: "Cell")
        view.addSubview(tableView)
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        data.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
        let number = data[indexPath.row]
        cell.textLabel?.text = "\(number)"
        return cell
    }
}
  • 제네릭스를 사용하여 추상화하고 사용하는곳에서는 간편하게 사용하게끔 구현하면?
    • 아래처럼 GenericTableView를 구현한 경우, Cell을 register하고 cellForRowAt같은 dataSource를 따로 구현하지 않고 단순하게 사용이 가능 (+재활용성 높은 코드)
class ViewController: UIViewController {
    let tableView = GenericTableView<CustomCell2>()
    let data = [1, 2, 3, 4, 5].map(String.init)

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.frame = view.bounds
        view.addSubview(tableView)
        tableView.updateData(data)
    }
}

GenericTableView 구현

  • GenericTableView에서는 Cell데이터에 관한 중복로직인 register(_:forCellReuseIdentifier:), numberOfRowsInSection, cellForRowAt과 같은 함수들을 내부에서 처리하도록 할 것

(불필요하게 tableView를 쓸때마다 중복으로 많이 들어가있는 코드)

tableView.register(CustomCell.self, forCellReuseIdentifier: "Cell")

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        data.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
        let number = data[indexPath.row]
        cell.textLabel?.text = "\(number)"
        return cell
    }
}
  • 이 코드를 내부에서 처리하기 위해서 CellType을 제네릭스로 만들고 셀 안에서 필요한 데이터 역시도 제네릭스로 구현
    • 셀의 가장 기본 상태와 기능은 셀이 표출할 데이터 타입과 해당 데이터 타입을 갱신해주는 configure 함수를 프로토콜로 표현
protocol TableViewCellable where Self: UITableViewCell {
    associatedtype D
    
    func configure(_ value: D)
}
  • 테이블 뷰에서는 이 타입을 제네릭스로 받으면 Cell 처리가 가능
final class GenericTableView<CellType: TableViewCellable>: UITableView, UITableViewDataSource {
}
  • 내부적으로 필요한 것들
    • register할때 필요한 CellType의 타입값: CellType.self
    • 셀에 표출할 데이터소스: CellType.D
    • register할때 필요한 cellID
private var cellType = CellType.self
private var cellDatas = [CellType.D]()
private var cellID: String {
    String(describing: cellType)
}
  • 이 값을을 활용하여 tableView DataSource처리하여 구현
final class GenericTableView<CellType: TableViewCellable>: UITableView, UITableViewDataSource {
    private var cellType = CellType.self
    private var cellDatas = [CellType.D]()
    private var cellID: String {
        String(describing: cellType)
    }

    init() {
        super.init(frame: .zero, style: .plain)
        dataSource = self
        register(cellType, forCellReuseIdentifier: cellID)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func updateData(_ datas: [CellType.D]) {
        cellDatas = datas
        reloadData()
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        cellDatas.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as? CellType
        else { return UITableViewCell() }
        
        cell.configure(cellDatas[indexPath.row])
        return cell
    }
}
  • 이 tableView를 사용하는 커스텀 셀을 구현할 경우 단순히 TableViewCellable만 conform하여 구현
class CustomCell2: UITableViewCell, TableViewCellable {
    typealias D = String
    
    private let titleLabel = {
        let l = UILabel()
        l.textColor = .black
        l.font = .systemFont(ofSize: 24)
        l.translatesAutoresizingMaskIntoConstraints = false
        return l
    }()
    func configure(_ value: String) {
        titleLabel.text = value
    }
    ...
}
  • 사용하는쪽)
class ViewController: UIViewController {
    let tableView = GenericTableView<CustomCell2>()
    let data = [1, 2, 3, 4, 5].map(String.init)

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.frame = view.bounds
        view.addSubview(tableView)
        tableView.updateData(data)
    }
}

완료)

읽어보면 좋은 글) 프로토콜에 제네릭스 사용하여 추상화하기 포스팅 글

 

* 전체코드: https://github.com/JK0369/ExAbstract_1

* 참고

- https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/

Comments