enum 공부를 다시 하면서 필요성과 유용함에 감탄을 금치 못하고 있다. 과제와 코드 작업에서 활용해본 case를 공유해보려한다.
열거, 즉 정해진 범위 내에서의 데이터만 다루겠다는 목적성이 있다.
(Array, Dictionary, Set처럼 데이터가 삭제, 추가되지 않고 무한정 나열하지 않는다.)
class와 struct와 다르게 사용자가 직접 사용 범위에 한계를 두고 쓸 때 활용한다.
Model custom types that define a list of possible values.
a common type for a group of related values and enables you to work with those values in a type-safe way within your code.
사용자가 직접적으로 사용 방법을 정하고 (사용 방식에 한계를 두고) 활용한다는 의미는 열거할 수 있는 각 경우에 새로운 값이나 데이터를 연결할 수 있음을 내포한다.
참고) Array를 비롯한 다른 collection들과 달리 Enum type은 Compile time 때 에러 감지를 한다.
그래서 등장하는 rawValue, 원시값
열거형 타입의 각 경우(case)에 대하여 그 자체로도 활용할 수 있지만, 새로운 값을 할당해서 다른 방향으로도 활용할 수 있다. 또한 각 case, 멤버와 값을 분리했다고도 할 수 있다.
C언어로 배울 때만 해도 enum의 유용성에 큰 의문이 있었다.
(물론 C는 원시값으로 정수만 줄 수 있다.)
C enumerations assign related names to a set of integer values. Enumerations in Swift are much more flexible, and don’t have to provide a value for each case of the enumeration. If a value (known as a raw value) is provided for each enumeration case, the value can be a string, a character, or a value of any integer or floating-point type.
이로 인해 과거부터 주로 배열과 인덱싱 방식으로 데이터 구조를 활용했었다.
배열의 인덱스를 구해서 해당 정보와 매핑하기
이 방식은 인덱스가 수정되어 해당 정보와의 매핑이 깨지면 수작업으로 하나하나 찾아 들어가야 한다는 약점이 있다.
5가지 과일과 연계된 버튼이 존재한다. 각 버튼이 눌리면 count 횟수가 +1씩 되면서 debug로 print로 해당 과일이 몇번 눌렸는지 알려준다.
배열과 인덱스를 활용하면 다음과 같은 데이터 구조와 함수를 작성하게 된다.
CaseIterable
A type that provides a collection of all of its values.
enum Fruit: Int, CaseIterable {
case apple = 1
case banana
case orange
case watermelon
case grape
}
class ViewController: UIViewController {
//각 button의 tag는 1부터 5까지 설정
//첫번째 과일은 첫번째 버튼과 연결, 암시적으로 tag와 버튼 순서 연동
var countArray = [0, 0, 0, 0, 0]
//CaseIterable protocol에 conform해서 배열처럼 활용하기
var fruits = Fruit.allCases
//...중략...
@IBAction func buttonTapped(_ sender: UIButton) {
switch fruits[sender.tag-1] {
case .apple:
countArray[0] += 1
print("\(Fruit.apple) \(countArray[0])번 눌렸습니다.")
case .banana:
countArray[1] += 1
print("\(Fruit.banana) \(countArray[1])번 눌렸습니다.")
case .orange:
countArray[2] += 1
print("\(Emotion.soso) \(countArray[2])번 눌렸습니다.")
case .watermelon:
countArray[3] += 1
print("\(Emotion.sad) \(countArray[3])번 눌렸습니다.")
case .grape:
countArray[4] += 1
print("\(Emotion.tear) \(countArray[4])번 눌렸습니다.")
}
//tag를 좀 더 잘 활용하면 코드를 더 줄일 수 있긴 하다.
//countArray[sender.tag-1] += 1
//print("\(fruits[tag-1]) \(countArray[tag-1])번 눌렸습니다.")
}
인덱스와 해당 버튼 tag와의 연결이 변하지 않는다면 이 자체로도 작동은 충분히 된다. 문제는 다음과 같은 경우들이다.
enum과 버튼 tag와의 연결 관계가 깨져서 하나하나 연관된 부분을 찾아 수정해야 한다.
코드가 프로젝트 수정이나 관리의 기능을 못한다.
따라서 "한 곳"에서만 관리하겠다는 목적에서 시작, enum을 활용한다면 다음처럼 수정할 수 있다.
enum Fruit: Int {
case apple = 1
case banana
case orange
case watermelon
case grape
//연산 property 활용 (get)
//해당 Fruit case에 대해 String type으로 과일 이름 return
var name: String {
switch self {
case .apple:
return "apple"
case .banana:
return "banana"
case .orange:
return "orange"
case .watermelon:
return "watermelon"
case .grape:
return "grape"
}
}
}
class ViewController: UIViewController {
//과일 이름: 누른 횟수
var countDict = [String: Int]()
//...중략...
@IBAction func buttonTapped(_ sender: UIButton) {
//Fruit type의 instance
let fruit = Fruit(rawValue: sender.tag)
//instance의 property 접근
if let fruitName = fruit?.name {
if countDict[fruitName] != nil {
countDict[fruitName]! += 1
} else {
countDict[fruitName] = 1
}
print("\(fruitName) \(countDict[fruitName])번 눌렸습니다.")
}
}
}
이 경우, 버튼 tag가 변경되어도 enum에서 할당한 값만 바뀐 tag에 맞춰서 수정해주면 된다. 혹은 enum과 버튼 tag와의 연결 관계가 기억나지 않더라도 enum과 tag만 비교하면 된다.
마치 button의 tag를 자동화 과정에 연결해둬 신경쓰지 않아도 되는 편리함을 얻을 수 있다.
참고)
Data만 다루도록 DataManager를 만들어 singleton 패턴으로 활용하게 되면, DataManager 안에서 enum의 instance 호출은 가능하지만 반대는 불가하다.
class와 struct와 마찬가지로 instance 생성, property 접근은 동일하다.
class와 struct와 달리 instance를 생성할 수는 없지만, 각 case에 해당하는 property에는 접근할 수 있다.
Initializing from a Raw Value
If you define an enumeration with a raw-value type, the enumeration automatically receives an initializer that takes a value of the raw value’s type (as a parameter called rawValue) and returns either an enumeration case or nil. You can use this initializer to try to create a new instance of the enumeration.
수백만 개의 이미지 중 어떤 알고리즘을 통해 7개가 뽑힌다고 하자. 뽑힌 7개의 이미지를 TableViewCell에 설정한 UIImageView의 image로 활용하려고 한다.
struct CardNews {
let title: String
let releaseDate: String
}
enum ImageTitle: String {
//image의 이름과 CardNews의 title property 값이 동일하게 주어진다.
case breakingNews = "속보"
case tech = "테크"
case finance = "경제"
case politics = "정치"
case culture = "문화"
case weather = "날씨"
//각 image의 이름을 활용, constant로 UIImage instance 미리 생성해놓음
var cardImage: UIImage {
switch self {
case .breakingNews:
return breakingNewsImage!
case .tech:
return techImage!
case .finance:
return financeImage!
case .politics:
return politicsImage!
case .culture:
return cultureImage!
case .weather:
return weatherImage!
}
}
}
class CardNewsTableViewCell: UITableViewCell {
let data = CardNews(title: "속보", releaseDate: "23.07.28")
@IBOutlet weak var cardImageView: UIImageView!
//중략...
//TableView에서 IndexPath.row를 받아서 수행
func configCell(cell row: Int) {
let cardType = ImageTitle(rawValue: data.title)
cardImageView.image = cardType?.cardImage
}
}
Asset에 등록된 이미지의 이름과 CardNews 구조체의 title property의 값이 동일함을 전제로 시작한다.
따라서 먼저 constant로 UIImage(named: title)로 instance를 생성한다.
그 후, enum의 rawValue로 CardNews의 해당 case의 title property로 해당 case를 파악, 이를 통해 cardImage property에 접근, 해당 UIImage instance를 return으로 받는다.
이 방법 말고 만약 title 자체를 재활용하는데에 집중한다면 다음과 같이 수정할 수도 있다.
enum ImageTitle: String {
case breakingNews = "속보"
case tech = "테크"
case finance = "경제"
case politics = "정치"
case culture = "문화"
case weather = "날씨"
//각 case의 rawValue를 활용, 해당 case에 맞는 UIImage instance를 생성해서 return
var cardImage: UIImage {
return UIImage(named: self.rawValue)
}
}
UIImage instance가 다른 곳에서도 재활용될 수 있음을 염두하고 위처럼 작성할 수도 있다. 또한 반복되는 값 자체를 활용하는 것에 초점을 두고 아래처럼 작성할 수도 있다. 선택은 프로젝트의 방향과 팀 회의를 통해 어떤 것이든 결정될 수 있다.
참고)
enum 선언범주가 class 내부면 해당 class에서만 사용 가능하지만 외부에 정의되면 여러 class에서 사용 가능하다.
property는 함수의 parameter로도 활용할 수 있다.
배열 3개가 있다고 가정하면 각자 다른 상황에서 호출하기 위해 함수 3개가 필요했다.
enum으로 상황과 해당 배열을 연결하면 관련된 데이터 타입으로 return할 수 있다.
이렇게 되면 함수 하나로 여러 데이터 타입 case에 맞게 return이 가능하다.
참고