[Swift] 자네 열거형(enum)을 CaseIterable로 사용해본 적이 있는가?

2024. 8. 5. 11:28Swift, iOS Foundation

 

CaseIterable | Apple Developer Documentation

A type that provides a collection of all of its values.

developer.apple.com


혹시 Swift 코드에서 선언된 열거형(enum)을 보다가 CaseIterable이라는 키워드를 본 적이 있나?

CaseIterable을 직관적으로 해석해보면,
Case(열거형의 각 case) + Iterable(반복 가능한)의 합성어로 "열거형의 각 case를 반복 가능하게 사용할 수 있다"라고 해석된다.

<반복 가능한>이란 말이 조금 부자연스러워서 다르게 해석해 보면, Iterable을 <하나씩 순회할 수 있는>, <반환할 수 있는>
= 즉, 열거형의 각 Case를 Sequence Data의 형태로 사용할 수 있다는 것을 의미한다고 해석하면 더 와닿을 수 있다.

💡 CaseIterable열거형(enum)의 모든 case를 Collection으로 다룰 수 있게 해주는 프로토콜이다.


열거형의 모든 case를 Collection으로 다루면 뭐가 달라지는 건데?

  • 열거형의 모든 case를 for문 또는 forEach를 사용해서 쉽게 반복할 수 있게 된다 (= iterable).
  • Collection에서 사용하는 메서드나 Collection일 때 사용할 수 있는 고차함수를 자유롭게 사용할 수 있게 된다. (= 기능 확장)


CaseIterable을 채택한 enum형에서 allCases라는 속성을 사용하면 열거형을 Collection처럼 다룰 수 있다.

CompassDirection이라는 열거형(enum)을 사용하고 있는 아래 예시 코드를 살펴보자.
첫 번째는 allCases.count 메서드를 사용해 case의 전체 개수를 가져오고 있고, 두 번째는 map과 joined 함수를 사용해 각 case를 하나의 String으로 묶는 것을 볼 수 있다.

enum CompassDirection: CaseIterable {
    case north
    case south
    case east
    case west
}

print("There are \(CompassDirection.allCases.count) directions.")
// "There are 4 directions."

let caseList = CompassDirection.allCases
                               .map({ "\($0)" })
                               .joined(separator: ", ")
print(caseList)
// "north, south, east, west"


그럼 CaseIterable Protocol이 어떻게 선언되어 있는지 코드를 자세히 들여다볼까?

위에서 사용했던 allCases 속성은 Collection 타입의 associatedtype로 정의되어 있으며, 기본적으로 (연관 타입을 별도로 명시하지 않은 경우) 열거형 자신의 타입(Self)이 Collection의 타입으로 지정되는 것을 확인할 수 있다.

여기서 특별히 눈여겨봐야 할 것은 where 조건 부분이다.
모든 AllCases의 Element 타입이 Self 타입과 일치하는 경우에만 allCases 속성을 사용할 수 있다. (무조건 다 사용할 수 있는 게 아니다!)

public protocol CaseIterable {

    /// A type that can represent a collection of all values of this type.
    associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element

    /// A collection of all values of this type.
    static var allCases: Self.AllCases { get }
}

"CaseIterable을 무조건 다 사용할 수 있지 않다는 점"이 이해가 안 될 수도 있을 것 같아 조금 더 자세하게 설명해 보겠다.


아래의 NetworkResult라는 열거형에 붙인 CaseIterable 프로토콜은 정상적으로 작동하지 않는다.

그 이유는 success와 failure case 각각에 붙어있는 String과 Int 타입의 연관 값 (associated values) 때문이다.
여기서! 연관 값이란 열거형(enum)에 있는 각 Case가 추가적인 값을 가지도록 해 다양한 데이터를 표현할 수 있도록 하는 것을 말한다.

비상! 에러 발생! CaseIterable을 사용할 수가 없다!


"왜 allCases가 정상적으로 작동하지 않을까?"

쉽게 말해, case가 갖는 연관 값 = case가 갖는 추가적인 값에 대해서는 컴파일러가 생길 수 있는 모든 경우를 파악할 수 없기 때문이다.

case success, failure만 있을 경우에는 allCases Collection에 들어가는 항목이 success, failure 2개로 압축된다.
하지만, 연관값이 있을 경우에는 suceess(String)에서 String 타입에 들어가는 값을 한정 지을 수 없고 / failure(Int)에서 Int 타입에 들어가는 값을 한정할 수 없기 때문.


그래서 만약 allCases를 사용하고 싶다면,
아래 코드처럼 allCases를 직접 정의하는 부분을 만들고 + 각 연관값에 들어가게 될 실제 값을 명시해 주면 된다!

enum NetworkResult: CaseIterable {
    case success(String)
    case failure(Int)
    
    static var allCases: [Self] {
        return [.success("Success Message"), .failure(404)]
    }
}

결론적으로, CaseIterable은 열거형(enum)의 모든 case를 순차적으로 접근해야 하거나 / 전체 case의 count를 받아와야 하는 경우 유용하게 사용할 수 있다.

아래는 UITabBarController를 구성하는 데 있어 탭의 각 요소(연결 VC, title, icon)를 enum형으로 만들고, 이 각각의 case를 반복하면서 TabBarController의 하위 ViewController로 구성하는 상황을 나타내는 코드다.

TabBarItem.allCases.map을 통해 각 탭 항목을 모두 반복하며 TabBar에 연결될 VC와 UITabBarItem을 구성하고 있는 것을 확인할 수 있다.

enum TabBarItem: CaseIterable {
    case tab1, tab2, tab3
   
    var viewController: UIViewController {
        switch self {
        case .tab1: return FirstViewController()
        case .tab2: return SecondViewController()
        case .tab3: return ThirdViewController()
    }
    
    var title: String {
        switch self {
        case .tab1: return "First"
        case .tab2: return "Second"
        case .tab3: return "Third"
    }

    var icon: UIImage {
        switch self {
        case .tab1: return .iconTab1
        case .tab2: return .iconTab2
        case .tab3: return .iconTab3
        }
    }
}

// TabBarController
func setupTabBarControllers() {
    let viewControllers = TabBarItem.allCases.map { item -> UIViewController in
        let viewController = item.viewController
        viewController.tabBarItem = UITabBarItem(title: item.title, image: item.icon.withTintColor(.gray150), selectedImage: item.icon.withTintColor(.black900))
        return viewController
    }
    self.viewControllers = viewControllers
}


이 예제 외에도 TableView나 CollectionView에서 Section을 구성하는 상황이나,

SegmentedControl, PickerView 같이 큰 화면 내에 세부적인 요소를 순차적으로 포함시키는 상황에서 enum과 CaseIterable 프로토콜을 함께 사용해 -> allCases를 통한 Collection 접근을 사용할 수 있다!

오늘 설명한 CaseIterable로 더 확장되고 세련된 코드를 작성할 수 있는 여러분이 되길 바란다! 끝!