관리 메뉴

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

[iOS - swift] protocol 심화 - (protocol 응용, existential any, generics, cannot conform to 'Hashable', 의존성 분리) 본문

iOS 응용 (swift)

[iOS - swift] protocol 심화 - (protocol 응용, existential any, generics, cannot conform to 'Hashable', 의존성 분리)

jake-kim 2023. 4. 30. 23:53

existential any 개념

  • protocol에서 generic을 지정해줄 때 any 키워드를 사용하여 편리하게 generic 처리를 할 수 있는 것
protocol ABC {
    associatedtype MyType
}

// 기존 - 제네릭 선언해야함
func printValue<T: ABC>(type: T) {
    print(type)
}

// any 키워드 사용 - 제네릭을 따로 선언해주지 않아도 됨
func printValue2(type: any ABC) {
    print(type)
}

 

protocol 타입 다루기

  • DIP원칙과 테스트에 용이한 구성을 위해서 데이터 모델을 protocol로 참조되게 설정

ex) 모델이 MyItem이고, 이 모델은 MyItemable 프로토콜을 따르게 한 후 사용하는쪽에서는 MyItemable 프로토콜로만 참조되게 구현

protocol MyItemable: Hashable {
    var id: UUID { get }
    var value: Int { get }
}

struct MyItem: MyItemable {
    let id = UUID()
    let value: Int
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
  • 만약 UITableViewDiffableDataSource를 사용한다면, 이 안에 Hashable 타입을 넣어서 초기화가 필요
    • 프로토콜을 따르지 않게하면 아래처럼 MyItem하면 되지만 protocol로 표현하고 싶은 경우?
import UIKit

final class MyDataSource: UITableViewDiffableDataSource<Int, MyItem> {   
}
  • protocol로 변경한 경우 에러 발생
final class MyDataSource: UITableViewDiffableDataSource<Int, MyItemable> { // Error!
    
}

  • 사전지식) MyItemable로 넣었지만 암묵적으로 any MyItemable로 선언됨
    • any로 선언되는 이유는 hashable안에 있는 Hasher의 combine 타입이 제네릭이라 any가 필요한 것

  • 문제) MyItemable은 분명 정의하는 곳에서 Hashable을 따르게 했지만, UITableViewDiffableDataSource에서 사용하면 Hashable을 준수하지 않다고 표출
    • 프로토콜을 타입으로 사용할 때, 정의할 때 당시 사용했던 protocol (Hashable)은 적용되지 않는 버그가 존재
protocol MyItemable: Hashable {
    var id: UUID { get }
    var value: Int { get }
}

  • 해결책
    • 현재 MyItemable 부분에 구현체를 넣어주어야 하는 상황
    • Hashable만을 따르고 있는 AnyHashable을 준수하는 Wrapper를 만들어서 그 안은 MyItemable을 가지고 있도록 구현하면 완료
import UIKit

enum MyItemableWrapper: Hashable {
    case item(any MyItemable)
    
    func hash(into hasher: inout Hasher) {
        switch self {
        case let .item(item):
            hasher.combine(item.id)
        }
    }
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case let (.item(lhsItem), .item(rhsItem)):
            return lhsItem.id == rhsItem.id
        }
    }
}

final class MyDataSource: UITableViewDiffableDataSource<Int, MyItemableWrapper> {
}
  • 사용하는 쪽 - 구체적인 타입 의존없이 프로토콜을 wrapping하고 있는 타입 그대로 사용 가능
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tableView = UITableView()
        let dataSource = MyDataSource(tableView: tableView) { tableView, indexPath, itemIdentifier in
            switch itemIdentifier {
            case let .item(item):
                print(item.value)
            }
            return UITableViewCell()
        }
    }
}

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

 

Comments