Devin.KR

Swift · 기본

Swift로 만드는 첫 앱 로직

열거형 - 연관값으로 상태 표현

case 와 원시값, 연관값, switch 로 모든 경우 처리, 열거형 메서드, CaseIterable, 상태 기계 모델링

개발자KR · 원고 갱신

이 장에서 배우는 것

앞 장에서 구조체와 클래스로 습관 기록 하나를 값 타입과 참조 타입으로 나눠 담아 보았다. 이번 장에서는 "여러 가지 중 하나"라는 성질을 타입으로 적는 열거형(enumeration)을 다룬다. 습관 앱에는 종류(물 마시기, 운동, 독서)처럼 후보가 정해진 값이 있고, 하루의 상태(시작 전, 진행 중, 달성, 건너뜀)처럼 상태마다 붙는 정보가 다른 값도 있다. 열거형은 이 둘을 같은 문법으로 표현한다. 화면 없이 로직만 만드는 이 책의 방식대로, 이번 장도 터미널에서 실행되는 하나의 파일로 끝낸다.

  • case 로 선택지를 정의하고, 원시값(raw value)으로 문자열이나 숫자와 연결한다.
  • 연관값(associated value)으로 case 마다 다른 정보를 담는다.
  • switch 로 모든 경우를 빠짐없이 처리하고, 컴파일러에게 누락을 검사시킨다.
  • 열거형에 속성과 메서드를 붙이고, CaseIterable 로 모든 case 를 순회한다.
  • 열거형 두 개로 하루 상태를 상태 기계(state machine)로 모델링한다.

문제 상황

습관 앱의 기록 코드를 처음 짜면 대개 문자열과 불리언 값으로 시작한다. 습관 종류는 "water" 같은 문자열로 들고 다니고, 하루 상태는 isStarted, isDone, isSkipped 세 개의 불리언으로 나타낸다. 처음에는 잘 돌아가지만 몇 가지 문제가 쌓인다.

첫째, 문자열은 오타를 막지 못한다. "watter" 를 넘겨도 컴파일러는 모르고, 앱이 돌아가는 중에야 통계가 이상하게 나온다. 둘째, 불리언 세 개는 여덟 가지 조합을 만들 수 있는데, 실제로 의미 있는 상태는 네 가지뿐이다. "달성했는데 건너뜀" 같은 모순된 조합이 코드 어디선가 만들어져도 아무도 막지 못한다. 셋째, 진행 중일 때만 "지금까지 몇 잔"이라는 정보가 필요하고 건너뜀일 때만 "이유"가 필요한데, 구조체 하나에 모든 필드를 넣으면 쓰이지 않는 필드가 계속 남는다.

열거형은 이 세 문제를 모두 타입 수준에서 줄인다. 가능한 값을 case 로 나열하면 오타는 컴파일 오류가 되고, 상태마다 필요한 값만 연관값으로 붙이면 모순된 조합은 아예 만들 수 없다.

case 와 원시값

case 로 선택지 나열하기

열거형은 enum 키워드로 선언하고, 가능한 값을 case 로 하나씩 적는다. 습관 종류를 이렇게 적을 수 있다.

enum HabitKind {
    case water
    case exercise
    case reading
}

let today = HabitKind.water
let another: HabitKind = .reading

타입이 이미 정해진 자리에서는 타입 이름을 생략하고 점만 써도 된다. another 처럼 타입을 적어 둔 변수에 값을 넣을 때가 그렇다. 열거형은 앞 장에서 본 구조체처럼 값 타입이어서, 다른 변수에 대입하면 복사된다.

원시값 붙이기

case 마다 문자열이나 정수를 하나씩 붙이고 싶을 때는 열거형 이름 뒤에 타입을 적는다. 이렇게 붙인 값을 원시값이라 부른다. 값을 따로 적지 않으면 문자열 원시값은 case 이름과 같고, 정수 원시값은 0부터 하나씩 늘어난다.

enum HabitKind: String {
    case water
    case exercise
    case reading
}

print(HabitKind.exercise.rawValue)   // exercise

원시값의 쓸모는 저장하거나 주고받을 때 드러난다. 파일에 기록을 남기거나 서버와 통신할 때는 문자열이 필요하다. 반대로 문자열에서 열거형을 만들 수도 있다. 이때 HabitKind(rawValue: "yoga") 처럼 case 에 없는 문자열이 들어올 수 있으므로 결과는 옵셔널이다. 값이 없을 수 있음을 타입으로 표현한다는 옵셔널의 원칙이 여기서 그대로 적용된다.

CaseIterable 로 모든 case 순회하기

"모든 습관 종류를 한 번씩 보여 주고 싶다"는 요구는 자주 나온다. 열거형 선언에 CaseIterable 을 적으면 컴파일러가 allCases 라는 배열을 자동으로 만들어 준다. 순서는 case 를 선언한 순서와 같아서 출력이 일정하다. case 를 하나 추가하면 allCases 도 저절로 늘어나므로, 목록을 따로 관리하다 하나 빠뜨리는 실수가 사라진다.

enum HabitKind: String, CaseIterable {
    case water
    case exercise
    case reading
}

print(HabitKind.allCases.count)   // 3

연관값과 switch

case 마다 다른 값 담기

원시값은 선언할 때 정해지고 같은 case 라면 늘 같다. 하지만 기록 하나에는 "3잔", "25분", "11쪽부터 30쪽까지"처럼 만들 때마다 달라지는 정보가 필요하다. 이럴 때 case 뒤에 괄호로 타입을 적어 연관값을 붙인다. case 마다 값의 개수와 타입이 달라도 된다.

enum Entry {
    case cups(Int)
    case minutes(Int)
    case pages(start: Int, end: Int)
}

let first = Entry.cups(3)
let second = Entry.pages(start: 11, end: 30)

연관값에는 구조체 필드처럼 라벨을 붙일 수 있다. 값이 두 개 이상이면 라벨을 적어 두는 편이 읽기 좋다. .pages(11, 30) 만 보고는 어느 쪽이 시작인지 알기 어렵기 때문이다.

원시값은 case 마다 선언할 때 정해진 값 하나이고, 연관값은 값을 만들 때마다 달라진다.
원시값과 연관값의 차이
구분원시값연관값
값이 정해지는 때enum 을 선언할 때case 값을 만들 때
같은 case 의 값항상 같다만들 때마다 다를 수 있다
값의 모양case 모두 같은 타입 하나case 마다 개수와 타입이 자유
값 꺼내기rawValue 속성switch 또는 if case 패턴

원시값을 지정한 열거형에는 연관값을 둘 수 없다. 둘은 서로 다른 용도이므로, 필요한 쪽 하나를 고른다.

switch 로 모든 경우 처리하기

연관값을 꺼내는 기본 방법은 switch 다. 앞 장에서 다룬 switch 를 열거형에 쓰면, 컴파일러가 모든 case 를 처리했는지 검사한다. 하나라도 빠지면 컴파일 오류다. 이 검사가 열거형의 가장 큰 이점이다.

func amount(of entry: Entry) -> Int {
    switch entry {
    case .cups(let count):
        return count
    case .minutes(let minutes):
        return minutes
    case .pages(let start, let end):
        return end - start + 1
    }
}

case 패턴 안의 let count 는 연관값을 새 상수에 묶는다. 값이 필요 없으면 case .cups: 처럼 괄호를 생략해도 된다. 여러 case 를 쉼표로 묶어 같은 처리를 줄 수도 있다. 한 가지 경우만 보면 될 때는 if case .cups(let count) = entry { ... } 로 짧게 쓴다.

모든 case 를 나열하는 대신 default 로 나머지를 한꺼번에 받을 수도 있다. 하지만 default 를 쓰면 나중에 case 가 추가돼도 컴파일러가 경고하지 않는다. 이 장의 코드는 case 를 모두 적고, default 는 쓰지 않는다.

열거형에 메서드 붙이기

열거형은 구조체처럼 계산 속성과 메서드를 가질 수 있다. 습관 종류마다 이름, 하루 목표량, 단위가 다르다면 switch 를 열거형 안에 한 번만 두고 밖에서는 속성으로 읽는다. 호출하는 쪽의 코드가 단순해지고, case 가 늘어날 때 고칠 곳이 열거형 한 곳으로 모인다.

열거형 자신을 바꾸는 메서드는 mutating 을 붙이고 self 에 새 값을 대입한다. 구조체 필드를 바꾸는 것과 달리, 열거형은 case 통째로 갈아 끼운다는 점이 다르다. 아래 완성 코드에서 이 형태를 볼 수 있다.

열거형으로 상태 기계 만들기

상태 기계란 "지금 어떤 상태인가"와 "어떤 사건이 일어났는가"를 입력으로 받아 "다음 상태"를 정하는 모델이다. 하루 단위의 습관 기록은 여기에 잘 맞는다. 상태는 네 가지로 나뉜다.

  • notStarted: 아직 기록이 없다.
  • inProgress(done:goal:): 기록은 있으나 목표에 못 미쳤다. 지금까지의 양과 목표를 가진다.
  • completed(total:): 목표를 채웠다. 총량을 가진다.
  • skipped(reason:): 오늘은 건너뛰기로 했다. 이유를 가진다.

사건도 열거형으로 적는다. 기록 추가(log), 건너뜀(skip), 초기화(reset) 세 가지다. 상태와 사건의 모든 조합에 대해 다음 상태가 무엇인지 표로 정리하면 설계가 한눈에 보인다.

현재 상태와 사건에 따른 다음 상태
현재 상태log(양)skip(이유)reset
notStarted양에 따라 진행 중 또는 달성건너뜀시작 전
inProgress양을 더한 뒤 진행 중 또는 달성건너뜀시작 전
completed변화 없음변화 없음시작 전
skipped변화 없음변화 없음시작 전
하루 상태는 시작 전에서 진행 중, 달성 또는 건너뜀으로 이동하고, reset 은 어느 상태에서든 시작 전으로 되돌린다.

이 표는 그대로 switch 가 된다. 상태와 사건을 튜플로 묶어 switch (self, event) 로 검사하면 표의 칸 하나가 case 하나에 대응한다. 컴파일러가 모든 조합을 검사하므로, 나중에 상태나 사건을 추가하면 처리하지 않은 칸이 곧바로 오류로 드러난다. 불리언 세 개로 만든 구조에서는 얻을 수 없던 안전망이다.

완성 코드

아래는 습관 종류, 기록, 하루 상태 전이, 한 주 집계를 한 파일에 모은 프로그램이다. main.swift 로 저장한다.

enum HabitKind: String, CaseIterable {
    case water
    case exercise
    case reading

    var title: String {
        switch self {
        case .water: "물 마시기"
        case .exercise: "운동"
        case .reading: "독서"
        }
    }

    var dailyGoal: Int {
        switch self {
        case .water: 8
        case .exercise: 30
        case .reading: 20
        }
    }

    var unit: String {
        switch self {
        case .water: "잔"
        case .exercise: "분"
        case .reading: "쪽"
        }
    }
}

enum Entry {
    case cups(Int)
    case minutes(Int)
    case pages(start: Int, end: Int)

    var kind: HabitKind {
        switch self {
        case .cups: .water
        case .minutes: .exercise
        case .pages: .reading
        }
    }

    var amount: Int {
        switch self {
        case .cups(let count): count
        case .minutes(let minutes): minutes
        case .pages(let start, let end): max(end - start + 1, 0)
        }
    }

    var text: String {
        switch self {
        case .cups(let count): "\(count)잔 마심"
        case .minutes(let minutes): "\(minutes)분 운동"
        case .pages(let start, let end): "\(start)쪽부터 \(end)쪽까지 읽음"
        }
    }
}

enum DayEvent {
    case log(Int)
    case skip(reason: String)
    case reset

    var text: String {
        switch self {
        case .log(let amount): "\(amount) 기록"
        case .skip: "건너뜀 요청"
        case .reset: "초기화"
        }
    }
}

enum DayState {
    case notStarted
    case inProgress(done: Int, goal: Int)
    case completed(total: Int)
    case skipped(reason: String)

    static func progressed(done: Int, goal: Int) -> DayState {
        if done >= goal {
            return .completed(total: done)
        }
        return .inProgress(done: done, goal: goal)
    }

    var summary: String {
        switch self {
        case .notStarted: "시작 전"
        case .inProgress(let done, let goal): "진행 중 \(done)/\(goal)"
        case .completed(let total): "달성 \(total)"
        case .skipped(let reason): "건너뜀(\(reason))"
        }
    }

    func applying(_ event: DayEvent, goal: Int) -> DayState {
        switch (self, event) {
        case (_, .reset):
            return .notStarted
        case (.notStarted, .log(let amount)):
            return DayState.progressed(done: amount, goal: goal)
        case (.inProgress(let done, _), .log(let amount)):
            return DayState.progressed(done: done + amount, goal: goal)
        case (.notStarted, .skip(let reason)), (.inProgress, .skip(let reason)):
            return .skipped(reason: reason)
        case (.completed, _), (.skipped, _):
            return self
        }
    }

    mutating func apply(_ event: DayEvent, goal: Int) {
        self = applying(event, goal: goal)
    }
}

print("== 습관 종류 ==")
for kind in HabitKind.allCases {
    print("\(kind.rawValue): \(kind.title) 목표 \(kind.dailyGoal)\(kind.unit)")
}
print("종류 수: \(HabitKind.allCases.count)")

let inputs = ["exercise", "yoga"]
for text in inputs {
    if let kind = HabitKind(rawValue: text) {
        print("\(text) -> \(kind.title)")
    } else {
        print("\(text) -> 알 수 없는 습관")
    }
}

print("== 기록 ==")
let entries: [Entry] = [.cups(3), .minutes(25), .pages(start: 11, end: 30), .cups(5)]
for entry in entries {
    print("\(entry.kind.title): \(entry.text) -> \(entry.amount)\(entry.kind.unit)")
}

print("== 하루 상태 변화 ==")
let goal = HabitKind.water.dailyGoal
let events: [DayEvent] = [.log(3), .log(4), .log(2), .log(1), .reset, .skip(reason: "여행")]
var state = DayState.notStarted
for (index, event) in events.enumerated() {
    state.apply(event, goal: goal)
    print("\(index + 1). \(event.text) -> \(state.summary)")
}

print("== 한 주 집계 ==")
let week: [DayState] = [
    .completed(total: 8),
    .inProgress(done: 5, goal: 8),
    .completed(total: 10),
    .skipped(reason: "출장"),
    .notStarted
]
var completedDays = 0
var skippedDays = 0
for day in week {
    switch day {
    case .completed:
        completedDays += 1
    case .skipped:
        skippedDays += 1
    case .notStarted, .inProgress:
        break
    }
}
print("달성 \(completedDays)일, 건너뜀 \(skippedDays)일")
if case .inProgress(let done, let target) = week[1] {
    print("남은 양: \(target - done)")
}

줄별 해설

HabitKind

enum HabitKind: String, CaseIterable 은 원시값 타입을 문자열로 정하고 allCases 를 켠다. case 에 값을 적지 않았으므로 원시값은 case 이름 그대로다. title, dailyGoal, unit 은 계산 속성이고, 각각 switch 로 case 마다 값을 돌려준다. switch 의 각 분기가 값 하나이면 return 을 생략할 수 있어서 한 줄로 적었다. case 를 하나라도 빼면 컴파일 오류가 난다.

Entry

세 case 는 연관값의 모양이 다르다. kind 는 연관값이 필요 없어 case .cups: 로 괄호를 생략했다. amount 는 쪽수의 경우 end - start + 1 로 읽은 쪽수를 구한다. 11쪽부터 30쪽까지는 20쪽이다. 시작이 끝보다 큰 잘못된 입력이 들어와도 음수가 되지 않도록 max 로 0 이상을 보장했다. 올바른 입력인지 검사하는 일은 오류 처리를 다루는 장에서 제대로 다룬다.

DayEvent

사건 세 가지를 열거형으로 두었다. text 는 출력용 문장이다. 열거형 값을 문자열 보간에 그대로 넣으면 log(3) 같은 내부 표현이 나오므로, 사용자에게 보일 문장은 이렇게 따로 만든다.

DayState

연관값이 있는 상태 열거형이다. progressed 는 양이 목표 이상이면 completed, 아니면 inProgress 를 만든다. 진행 중과 달성의 분기를 이 함수 한 곳에 모아서, 전이 규칙에서 같은 비교를 되풀이하지 않는다. summary 는 상태별 문장을 만든다.

applying 은 앞의 전이 표와 같은 순서로 쓴 switch 다. 첫 case (_, .reset) 의 밑줄은 상태가 무엇이든 받는다는 뜻이다. 그 아래 (.inProgress(let done, _), .log(let amount)) 는 상태에서 지금까지의 양을, 사건에서 새 양을 동시에 꺼낸다. 건너뜀 case 는 두 상태가 같은 처리를 하므로 쉼표로 묶었고, 묶인 패턴이 같은 이름 reason 을 같은 타입으로 묶어야 한다는 규칙을 지켰다. 마지막 case 는 달성과 건너뜀이 어떤 사건을 받아도 self 를 돌려준다. 이 함수에는 default 가 없지만 컴파일러가 모든 조합을 확인한다.

apply 는 mutating 메서드로, applying 이 돌려준 새 상태를 self 에 대입한다. 호출하는 쪽은 state.apply(...) 한 줄이면 된다.

실행 부분

첫 블록은 allCases 로 종류를 순회하고 HabitKind(rawValue:) 의 옵셔널 결과를 if let 으로 푼다. yoga 는 case 에 없으므로 else 로 간다. 둘째 블록은 연관값이 다른 기록 네 개를 같은 배열에 담아, 같은 속성으로 읽는다. 셋째 블록은 물 마시기 목표 8잔을 기준으로 사건 여섯 개를 차례로 적용한다. 3, 7(3+4)까지는 진행 중이고, 9(7+2)가 되는 순간 달성이 된다. 달성 뒤의 기록은 무시되고, 초기화로 처음 상태로 돌아가며, 건너뜀 사건이 마지막 상태를 정한다. 마지막 블록은 한 주의 상태 배열을 switch 로 세고, if case 로 진행 중인 날의 남은 양만 꺼낸다. 8 - 5 가 아니라 목표 8에서 현재 5를 뺀 3이다.

실행 결과

$ swiftc main.swift -o app
$ ./app
== 습관 종류 ==
water: 물 마시기 목표 8잔
exercise: 운동 목표 30분
reading: 독서 목표 20쪽
종류 수: 3
exercise -> 운동
yoga -> 알 수 없는 습관
== 기록 ==
물 마시기: 3잔 마심 -> 3잔
운동: 25분 운동 -> 25분
독서: 11쪽부터 30쪽까지 읽음 -> 20쪽
물 마시기: 5잔 마심 -> 5잔
== 하루 상태 변화 ==
1. 3 기록 -> 진행 중 3/8
2. 4 기록 -> 진행 중 7/8
3. 2 기록 -> 달성 9
4. 1 기록 -> 달성 9
5. 초기화 -> 시작 전
6. 건너뜀 요청 -> 건너뜀(여행)
== 한 주 집계 ==
달성 2일, 건너뜀 1일
남은 양: 3

실무에서 자주 틀리는 것

default 로 나머지를 뭉뚱그리기

case 가 많으면 몇 개만 적고 default 로 마무리하고 싶어진다. 당장은 편하지만, 나중에 case 가 늘 때 컴파일러가 알려 주지 않는다.

// 틀린 코드: 독서 단위가 "분"으로 나온다
func unit(of kind: HabitKind) -> String {
    switch kind {
    case .water: return "잔"
    default: return "분"
    }
}

독서가 나중에 추가된 case 라면 이 함수는 조용히 "분"을 돌려준다. 모든 case 를 적으면 새 case 를 추가하는 순간 이 switch 가 오류를 내고, 고칠 곳을 알려 준다.

// 고친 코드
func unit(of kind: HabitKind) -> String {
    switch kind {
    case .water: return "잔"
    case .exercise: return "분"
    case .reading: return "쪽"
    }
}

원시값 열거형에 연관값 달기

원시값과 연관값을 한 열거형에 함께 쓰려는 시도다. 컴파일러가 거부한다.

// 틀린 코드: 컴파일되지 않는다
enum Entry: Int {
    case cups(Int)
    case minutes(Int)
}

원시값은 case 마다 하나로 고정된 값이고, 연관값은 만들 때마다 달라지는 값이어서 역할이 겹치지 않는다. 저장용 식별자가 필요하면 종류를 나타내는 열거형(HabitKind)에 원시값을 두고, 값이 붙는 쪽(Entry)에는 원시값 없이 연관값만 둔다. 이 장의 완성 코드가 그 구조다.

// 고친 코드
enum Entry {
    case cups(Int)
    case minutes(Int)
}

상태를 불리언 여러 개로 표현하기

불리언 세 개는 조합이 여덟 가지인데 쓸 수 있는 상태는 네 가지다. 나머지 조합은 버그로 이어진다.

// 틀린 코드: 달성이면서 건너뜀인 기록을 만들 수 있다
struct DayRecord {
    var isStarted = false
    var isDone = false
    var isSkipped = false
}

상태를 열거형 하나로 바꾸면 한 번에 한 case 만 가질 수 있고, 건너뜀 이유 같은 부가 정보도 해당 case 안에 둘 수 있다.

// 고친 코드
struct DayRecord {
    var state: DayState = .notStarted
}

알 수 없는 rawValue 를 기본 case 로 덮기

옵셔널을 풀기 귀찮아서 ?? 로 기본값을 넣는 방식이다.

// 틀린 코드: "yoga" 같은 오타나 새 종류가 물 마시기로 기록된다
let kind = HabitKind(rawValue: text) ?? .water

입력이 잘못됐다는 사실이 사라지고 엉뚱한 통계가 쌓인다. 값이 없을 때 무엇을 할지 분명히 정한다.

// 고친 코드
guard let kind = HabitKind(rawValue: text) else {
    print("\(text) -> 알 수 없는 습관")
    return
}

이 코드는 함수 안에서 쓰는 형태다. 최상위 코드에서는 return 대신 if let 의 else 쪽에서 처리하면 된다.

한눈에 보기

열거형 기능 정리
기능문법쓰는 때
case 정의case water가능한 값이 정해져 있을 때
원시값enum HabitKind: String저장·전송용 문자열이나 숫자가 필요할 때
원시값으로 만들기HabitKind(rawValue: text)외부 입력을 열거형으로 바꿀 때(결과는 옵셔널)
연관값case cups(Int)case 마다 다른 정보를 담을 때
모든 case 처리switch 에 case 모두 나열누락을 컴파일러가 검사하게 할 때
값 하나만 확인if case .a(let x) = value한 가지 경우만 볼 때
전체 순회CaseIterable, allCases모든 case 를 목록으로 쓸 때
자기 값 바꾸기mutating func, self = …상태를 갈아 끼울 때
상태를 표현하는 두 방식 비교
항목불리언 여러 개열거형
가능한 조합2의 개수 제곱, 대부분 무의미case 수만큼
부가 정보쓰이지 않는 필드가 남는다해당 case 의 연관값으로만 존재
상태 추가 시빠뜨려도 모른다처리하지 않은 switch 가 오류로 나온다

열거형 문법의 전체 목록은 Swift 공식 문서의 Enumerations 항목에서 확인할 수 있다. 다음 장에서 다룰 프로토콜은 이 장의 title, unit 같은 공통 속성을 "약속"으로 묶는 방법을 보여 준다.

연습 문제

  1. HabitKind 에 meditation case 를 추가한다. 목표는 10분, 단위는 "분", 이름은 "명상"이다. 추가한 직후 컴파일하면 어떤 일이 생기는지 설명하고, 고쳐서 실행되게 만든다.
  2. 월요일을 1로 시작하는 정수 원시값을 가진 Weekday 열거형(월~일, CaseIterable)을 만들고, 주말이면 true 를 돌려주는 isWeekend 속성을 추가한다. 주말의 원시값만 모아 출력하는 코드를 쓴다.
  3. 완성 코드의 DayState.applying 을 고쳐서, 달성 상태에서도 log 사건이 오면 총량에 양을 더하도록 한다. 이렇게 하면 실행 결과의 4번째 줄이 어떻게 바뀌는가.
  4. Entry 를 받아 오늘 목표를 채웠는지 Bool 로 돌려주는 함수 isGoalMet(_:) 를 쓴다. .cups(8) 과 .minutes(20) 의 결과를 예상한다.

정답과 해설

1번. title, dailyGoal, unit 세 속성의 switch 가 모두 "switch must be exhaustive" 오류를 낸다. 세 곳에 meditation 을 처리하지 않았기 때문이다. 각 switch 에 분기를 추가하면 된다. 이 오류가 바로 열거형의 장점이며, default 를 썼다면 이 오류는 나지 않고 잘못된 값이 조용히 쓰였을 것이다.

case meditation

// title
case .meditation: "명상"
// dailyGoal
case .meditation: 10
// unit
case .meditation: "분"

allCases 는 자동으로 4개가 되므로 습관 종류 출력에도 명상이 포함된다.

2번.

enum Weekday: Int, CaseIterable {
    case monday = 1, tuesday, wednesday, thursday, friday, saturday, sunday

    var isWeekend: Bool {
        switch self {
        case .saturday, .sunday: true
        case .monday, .tuesday, .wednesday, .thursday, .friday: false
        }
    }
}

let weekendValues = Weekday.allCases.filter { $0.isWeekend }.map { $0.rawValue }
print(weekendValues)   // [6, 7]

첫 case 에만 1을 적으면 뒤 case 는 2, 3, …으로 이어진다. 토요일이 6, 일요일이 7이다.

3번. 일반 규칙 (.completed, _) 보다 앞에 달성 상태의 log 를 따로 적는다. switch 는 위에서부터 맞는 첫 case 를 고르므로 순서가 중요하다.

case (.completed(let total), .log(let amount)):
    return .completed(total: total + amount)
case (.completed, _), (.skipped, _):
    return self

이제 4번째 줄은 4. 1 기록 -> 달성 10 이 된다. 달성 상태가 9에서 1을 더해 10이 되기 때문이다. 5번째 줄의 초기화는 그대로 시작 전으로 돌린다.

4번.

func isGoalMet(_ entry: Entry) -> Bool {
    entry.amount >= entry.kind.dailyGoal
}

.cups(8) 은 8 이상 8이므로 true, .minutes(20) 은 목표 30에 못 미쳐 false 다. 연관값 처리는 amount 속성 안에 이미 들어 있어서, 이 함수는 switch 없이 두 속성만 비교한다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.