안녕하세요.
오늘은 enum에서 enum type의 모든 case를 Collect해서 배열로 반환할 수 있는 protocol인 CaseIterable을 소개하려고 합니다. 그리고 RawRepresentable도 소개하려고 합니다. Enum 타입으로 case에 rawValue, associated value 뿐 아니라 더 다양하게 활용할 수 있는 protocol이 제공되는 Swift가 정말 좋다고 느껴집니다: )
CaseIterable Protocol
CaseIterable은 protocol입니다.
Protocol 장점은 상속이 되지 않는 값 타입 자료형인 Enum에 여러 개의 protocol을 채택할 수 있습니다.
allCases의 타입은 Self.AllCases입니다. 이는 Collection을 준수하면서 [Self] 형태의 배열입니다. 그렇기에 배열에 관련된 함수도 사용할 수 있습니다.
CaseIterable은 enum에서 정의한 enum type's 모든 cases를 allCases 프로퍼티를 통해 반환합니다.
Enum case에 associated values가 있을 경우 모든 경우를 반환하기 위한 로직을 allCases의 연산프로퍼티(computed property)를 직접 정의해야 합니다.
@available에 따라 특정 case가 사용되는 경우 컴파일러는 자동적으로 allCases를 생성하지 않아 직접 정의해야 합니다.
그 이외의 경우는 enum case's assocaited value, 특정 case를 @available 어노테이션으로 제약하지 않은 경우엔 컴파일러가 자동으로 enum's cases에 대해서 allCases를 제공해줍니다.
CaseIterable Protocol 활용
기본적으로 associated type가 없을 경우 컴파일러가 자동으로 allCases를 구현합니다. (enum case's associated values or @avaliable 어노테이션이 없기 때문입니다.)
enum TravelThemeType: CaseIterable {
case all
case season
case region
case travelTheme
case partner
}
let travelThemeType: [TravelThemeType] = TravelThemeType.allCases
TravelThemeType에서 준수한 CaseIterable protocol 덕에 자동으로 컴파일러가 구현한 allCases를 이용해 각각의 case 글자 길이가 6 이상인 case 개수를 출력하려고 합니다.
print(travelThemeType
.map { String(describing: $0) } // ["all", "season", "region", "travelTheme", "partner"]
.filter{ $0.count > 6 }
.count) // 2 출력
방금은 String(describing:)을 사용했는데요.
Swift의 enum은 다른 언어의 열거형과 다르게 case에 대해 rawValue를 갖을 수 있는 정말 강력한 장점이 있습니다. (TMI: C언어로 테트리스 만들 때 문자열은 안되나,, 너무 아쉬웠었는데..)
RawValue를 지정하기 위해 enumeration에 raw type을 지정할 수 있고 String, Int와 같은 타입을 raw type로 지정할 수 있습니다.
RawRepresentable Protocol 활용
// Swift.Protocols 라이브러리
public protocol RawRepresentable<RawValue> {
associatedtype RawValue
init?(rawValue: Self.RawValue)
var rawValue: Self.RawValue { get }
}
Enum에 String등의 rawType을 지정할 경우 컴파일러는 RawRepresentable protocol을 자동으로 준수합니다.
enum TravelThemeType: String, CaseIterable { ... } // raw type으로 String 지정
raw type을 String으로 enum에 지정했습니다. 그래서 이전 code snippet에서 map 고차함수에서 String(describing: $0)을 통해 변환하지 않아도 됩니다. 컴파일러가 RawRepresentable을 자동으로 준수하기 때문입니다.
print(travelThemeType
.map { $0.rawValue } // ["all", "season", "region", "travelTheme", "partner"]
.filter { $0.count > 6 }
.count) // 2
그럼 위와 같이 rawValue로 case의 raw type에 대한 값을 얻어올 수 있습니다.
반대로 RawRepresentable protocol을 준수해 rawValue computed property { get } 을 직접 정의 할 수 있습니다.
// MARK: - RawRepresentable
extension TravelThemeType: RawRepresentable {
var rawValue: String {
switch self {
case .all:
return "전체"
case .season:
return "계절"
case .region:
return "지역"
case .travelTheme:
return "여행 테마"
case .partner:
return "동반자"
}
}
}
print(travelThemeType.map { $0.rawValue })
// 출력: ["전체", "계절", "지역", "여행 테마", "동반자"]
물론 이를 활용해 enum case에 associated values가 있는 경우 raw Type에 일치하는 특정 value만 반환해도되고 다양하게 정의할 수 있습니다.
CaseIterable Protocol 준수할 때, raw type과 associated value가 있을 경우
enum Season: String, CaseIterable {
case spring = "봄"
case summer = "여름"
case fall = "가을"
case winter = "겨울"
static var count: Int {
return Self.allCases.count // 컴파일러가 자동으로 allCases 구현했기에 이와 같이 활용 가능
}
}
enum TravelThemeType: String, CaseIterable {
case all
case season(Season) // Error: Enum with raw type cannot have cases with arguments
case region
case travelTheme
case partner
static var allCases: [TravelThemeType] {
return [
.all,
.season(Season.spring),
.region,
.travelTheme,
.partner]
}
}
우리나라 계절은 크게 봄 여름 가을 겨울이 있습니다. Enum 타입으로 지정한 후 raw type을 String으로 선언한 후 특정 case의 associated value를 지정한 경우 raw type과 case의 associated value를 같이 선언할 수 없습니다.
참고로 Season의 allCases를 다양하게 활용할 수 있습니다. allCases는 static이라는 점을 유의해야 합니다.
// MARK: - RawRepresentable
extension TravelThemeType: RawRepresentable {
init?(rawValue: String) {
switch rawValue {
case "전체":
self = .all
case "계절":
self = .season(Season.spring)
case "지역":
self = .region
case "여행 테마":
self = .travelTheme
case "동반자":
self = .partner
default:
return nil
}
}
var rawValue: String {
switch self {
case .all: return "전체"
case .season: return "계절"
case .region: return "지역"
case .travelTheme: return "여행 테마"
case .partner: return "동반자"
}
}
}
print(travelThemeType.map { $0.rawValue }) // ["전체", "계절", "지역", "여행 테마", "동반자"]
이 경우 RawRepresentable을 extension으로 구현해야 합니다. 위에 코드처럼 RawRepresentable을 준수했기에, TravelThemeType에 선언했던 raw type인 String을 제거하면 case에 associated value가 있는 경우에도 rawValue를 직접 구현해 사용할 수 있습니다.
저는 allCases를 통해 TravelThemeType의 개수나 associated value를 제외한 case가 의미하는 rawValue를 얻어야 하기에 위와 같이 활용했습니다.
CaseIterable Protocol 과 , RawRepresentable 활용하기
앞으로 Season뿐 아니라 Region, Travel Theme등 여러 case를 enum으로 정의하고 때에 따라 카테고리 메뉴로 만들 것입니다. 이 enum들의 공통점은 CaseIterable 프로토콜을 준수하고, enum의 특징인 RawRepresentable의 rawValue를 이용해서 메뉴로 만들려고 합니다.
extension Season {
static var toKoreanList: [String] {
Self.allCases.map { $0.rawValue }
}
static var count: Int {
Self.allCases.count
}
}
// 17개 지역을 case로 지정한 enum
extension Region {
static var toKoreanList: [String] {
Self.allCases.map { $0.rawValue }
}
static var count: Int {
Self.allCases.count
}
}
extension TravelThemeType {
static var toKoreanList: [String] {
Self.allCases.map { $0.rawValue }
}
static var count: Int {
Self.allCases.count
}
}
...
이 enum들의 전부 공통점은 RawRepresentable, CaseIterable을 활용한다는 점입니다... static computed property들이 중복됨으로 차라리 CaseIterable을 extension해서 이들의 공통적인 특징을 기반으로 하는 static computed property를 적용해봤습니다.
extension CaseIterable where Self: RawRepresentable, Self.RawValue == String {
static var toKoreanList: [String] {
allCases.map { $0.rawValue }
}
static var count: Int {
Self.allCases.count
}
}
이 경우 CaseIterable을 채택헤 준수하는 타입(Self)이 RawRepresentable 타입인 경우, 대표적으로 enum이 있습니다. 이 경우에 toKoreanList, count라는 두 개의 static computed property를 사용할 수 있습니다. 그렇다면 이전에 각각의 enum에 exntesion으로 toKoreanList, count를 정의했던 중복 코드는 제거할 수 있습니다: ]
이번 포스트에선 enum에 대한 아주 짧은 개념과 CaseIterable, RawRepresentable을 소개했는데요. 틀린 부분 발견시 알려주신다면 정말 감사합니다.