1. CaseIterable і allCases
Коли ви пишете невеликий консольний застосунок, спокуса дуже велика: «ну список команд невеликий, зараз просто надрукую його як текст». На старті це здається безпечним, але в такого підходу є неприємна властивість: він ламається не відразу, а після змін.
Уявіть: ви додали нову команду .stats, але забули оновити текст help. Застосунок компілюється, запускається, та користувачі не дізнаються про нову команду. Або ще гірше: ви видалили команду, а help усе ще її показує. Це не «фатальна помилка», але вже технічний борг — маленький, липкий і неприємний.
Ідеальна мета — щоб список команд для help автоматично отримувався з enum, а будь-яке додавання або видалення case одразу відображалося в довідці. Саме це й робить CaseIterable.
Що таке CaseIterable і як його підключити
CaseIterable — це протокол стандартної бібліотеки Swift, який означає: «у цього типу є скінченний набір значень, і його можна перелічити». Для enum без додаткових даних компілятор уміє сам генерувати реалізацію — але лише якщо ви явно про це попросите.
За контрактом це виглядає так: у типу зʼявляється статична властивість allCases, яка повертає колекцію всіх варіантів. Формально протокол улаштований приблизно так: associatedtype AllCases: Collection і static var allCases: AllCases { get }.
Важливо: автоматична генерація працює для простих enum, тобто таких, де варіанти не зберігають додаткових значень (ми поки що працюємо саме з такими). Також компілятор синтезує allCases лише тоді, коли відповідність протоколу вказано прямо в оголошенні enum.
Давайте почнемо з найкоротшого прикладу.
import Foundation
enum Command: CaseIterable {
case help, exit
}
print(Command.allCases.count) // 2
Тут уже видно нову ідею: список варіантів — це властивість типу, а не конкретного значення.
allCases — властивість типу, а не екземпляра
У Swift і взагалі в мовах зі статичними членами діє проста логіка: якщо річ стосується всіх можливих значень типу, їй місце на типі. allCases — саме така річ: це «всі варіанти команди взагалі», а не «варіанти конкретної команди .help».
Тому ми пишемо Command.allCases, а не cmd.allCases.
import Foundation
enum Mood: CaseIterable {
case happy, neutral, sad
}
let m: Mood = .happy
print(Mood.allCases.count) // 3
// print(m.allCases) // ❌ так не можна: allCases — не властивість екземпляра
Якщо ви зловите себе на думці «ех, шкода, що не можна m.allCases», не хвилюйтеся: це як запитати в яблука список усіх фруктів. Яблуко, звісно, старається, але лапок у нього немає.
allCases — колекція, а не обовʼязково Array
Коли ви бачите allCases, дуже хочеться подумати: «це масив». У побуті так і можна думати, але з важливим застереженням: за контрактом allCases повертає якусь колекцію, а не обовʼязково масив Enum.
Практичний висновок простий: ви можете робити те, що вміє колекція, — наприклад, дізнаватися count і проходитися for-in.
import Foundation
enum Command: CaseIterable {
case help, exit
}
for cmd in Command.allCases {
print(cmd)
}
// help
// exit
Якщо вам раптом дуже потрібне індексування, як у масиву, наприклад «команда під номером 2», тоді ви можете явно перетворити allCases на масив.
import Foundation
enum Mood: CaseIterable {
case happy, neutral, sad
}
let list = Array(Mood.allCases)
print(list[0]) // happy
Щоб закріпити, ось маленька таблиця «що можна робити» з allCases:
| Що робимо | Приклад | Навіщо це в CLI |
|---|---|---|
| Дізнатися кількість | |
«Скільки команд підтримуємо?» |
| Перебрати | |
Виведення довідки |
| Отримати нумерацію | |
Зручна довідка зі списком 1..N |
| Індексувати | |
Меню вибору за номером, якщо раптом знадобиться |
2. Приклад: команди CLI
Від цього місця зберемо один мініпроєкт: консольний застосунок «бібліотека» — поки що дуже простий. Ми ще не будуємо ідеальну архітектуру та не зберігаємо книги у файлах. Наша мета сьогодні скромніша: зробити список команд і довідку, яка оновлюється автоматично.
Спочатку оголосимо список команд. Ми хочемо, щоб команди були стабільно представлені рядками, бо користувач вводить текст. Отже: RawRepresentable через String і CaseIterable для списку.
import Foundation
enum LibraryCommand: String, CaseIterable {
case help
case add
case list
case remove
case exit
}
Тут важливо: rawValue автоматично дорівнює імені цього caseʼа (наприклад, "add"), тому що ми використовуємо String-raw-values і не задаємо їх вручну.
3. Help/довідка з allCases
Просте виведення help
Тепер зробимо функцію, яка друкує список команд. Зверніть увагу: ми не пишемо вручну "help, add, list...". Замість цього беремо LibraryCommand.allCases і виводимо кожну команду.
import Foundation
func printHelp() {
print("Доступні команди:")
for cmd in LibraryCommand.allCases {
print("- \(cmd.rawValue)")
}
}
printHelp()
Вивід буде приблизно такий:
Доступні команди:
- help
- add
- list
- remove
- exit
З погляду підтримки коду це вже перемога: додали новий case — help оновився автоматично. Видалили case — help теж оновився.
Нумерована довідка
У CLI люди люблять списки «1) 2) 3)», бо так їх легше читати. Для цього використаємо enumerated().
import Foundation
func printHelp() {
print("Доступні команди:")
for (i, cmd) in LibraryCommand.allCases.enumerated() {
print("\(i + 1). \(cmd.rawValue)")
}
}
printHelp()
Тут i починається з 0, тому ми друкуємо i + 1. Це класичний момент «індекси з нуля» — і одна з найдавніших причин, чому програмісти іноді виглядають втомленими.
Мінісхема: enum → allCases → help
Щоб закріпити звʼязок «enum → allCases → help», корисно уявити це як маленький конвеєр:
flowchart LR
A["enum LibraryCommand"] --> B["LibraryCommand.allCases"]
B --> C["for-in / enumerated()"]
C --> D["виведення довідки/help"]
Сенс цієї схеми в тому, що help — не окремий «шматок тексту», а похідна від типу команд.
Чому CaseIterable особливо корисний для довідки
У CLI команда help — це не прикраса. Це «вихід із лабіринту», коли користувач або ви самі через тиждень забули синтаксис. І CaseIterable дозволяє реалізувати help так, щоб він був: узгодженим із реальністю, стійким до змін і з мінімумом дублювання коду. Ми не тримаємо окремий список команд вручну.
Якщо коротко: CaseIterable допомагає тримати одне джерело правди. Джерело правди — це enum LibraryCommand. А help — це просто «подання» цього enum для користувача.
Саме тому в Swift CaseIterable улаштований як opt-in протокол із allCases: ви явно кажете, що тип скінченний і його можна перелічувати, а потім користуєтеся цим як звичайною колекцією.
4. Розбір введення в інтерактивному циклі
Тепер зберемо мініінтерактив: застосунок запитує команду, читає рядок, намагається розпізнати його як LibraryCommand, а якщо не вдається — друкує підказку і help.
Зверніть увагу: ми використовуємо init?(rawValue:), який повертає Optional. Це чесна модель зовнішнього світу: користувач може ввести будь-що, а не лише «правильні» команди.
Читання рядка і спроба розпізнати команду
import Foundation
while true {
print("Введіть команду (help для довідки):", terminator: " ")
let input = readLine() ?? ""
if let cmd = LibraryCommand(rawValue: input) {
print("Гаразд, команда: \(cmd.rawValue)")
} else {
print("Невідома команда: \(input)")
printHelp()
}
}
Так, цикл нескінченний — ми ще не навчили його зупинятися. Виправимо це прямо зараз, але обережно: зробимо мінімальну перевірку.
Вихід через .exit і обробка .help
import Foundation
while true {
print("Введіть команду (help для довідки):", terminator: " ")
let input = readLine() ?? ""
guard let cmd = LibraryCommand(rawValue: input) else {
print("Невідома команда: \(input)")
printHelp()
continue
}
if cmd == .exit {
print("Вихід із програми.")
break
}
if cmd == .help {
printHelp()
} else {
print("Команду прийнято: \(cmd.rawValue)")
}
}
Тепер у нас уже робочий CLI: він уміє показувати довідку, скаржитися на невідоме і виходити.
Нормалізація введення: lowercased()
Користувач може ввести HELP, Help, HeLp і очікувати, що ви його зрозумієте. Якщо формат команд жорстко задано як lowercase, можна зробити крок назустріч: приводити введення до нижнього регістру перед парсингом.
Це не обовʼязкова частина CaseIterable, але для CLI це дуже практичний крок. І він добре поєднується з raw-значеннями: raw-значення залишаються стабільними, а ми просто робимо введення більш терпимим.
import Foundation
let input = (readLine() ?? "").lowercased()
if let cmd = LibraryCommand(rawValue: input) {
print("Команда: \(cmd.rawValue)")
} else {
print("Невідома команда: \(input)")
}
Якщо ви так зробите, help і далі виводитиме команди в нижньому регістрі, що збігається з канонічною формою.
5. Типові помилки під час роботи з CaseIterable і allCases
Помилка №1: очікувати, що allCases зʼявиться «сам по собі».
Новачки часто думають: «Раз enum скінченний, значить у нього точно є список варіантів». Але в Swift потрібно явно вказати : CaseIterable. Без цього allCases не буде, бо участь у механізмі перелічення — усвідомлене рішення розробника, і компілятор не буде «магічно» робити це за вас без запиту.
Помилка №2: звертатися до allCases в екземпляра, а не в типу.
Плутають cmd.allCases і LibraryCommand.allCases. Це логічно неправильно: екземпляр — це одне значення, а allCases — це «всі можливі значення типу». Якщо ловите цю помилку, зупиніться і подумки замініть cmd на «яблуко», а LibraryCommand — на «фрукт».
Помилка №3: думати, що allCases — це обовʼязково [LibraryCommand].
За контрактом це не масив, а «якась колекція». Іноді все одно зручно робити Array(LibraryCommand.allCases), але краще не будувати код так, ніби allCases завжди індексується як масив. Перебір for-in майже завжди простіший і надійніший.
Помилка №4: намагатися використовувати CaseIterable для enum із даними всередині case.
Сьогодні ми працюємо лише з простими enum. Якщо в case зберігаються додаткові значення, автоматична генерація allCases не спрацює, бо «всі варіанти» стають нескінченними (наприклад, .setVolume(Int) — це скільки варіантів? Нескінченно). Тому в межах цієї теми тримайтеся простих case без параметрів.
Помилка №5: тримати список команд окремо й забувати його оновлювати.
Іноді роблять так: let commands = ["help", "add", "list"], а enum існує окремо. Це знову повертає вас у світ «двох джерел правди»: одне в enum, друге — в масиві рядків. Через кілька змін вони обовʼязково розʼїдуться. CaseIterable якраз і потрібний, щоб цього не ставалося.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ