Модель: расходы
Чему вы научитесь:
- собирать модель учёта расходов из перечисления, структуры и массива;
- считать итоги по категориям и печатать их в одном порядке при каждом запуске;
- разбирать строки вида
food 12.5и называть причину, если строка неверна; - видеть, что вы уже умеете на Swift и куда двигаться дальше.

Представьте тетрадь расходов. На её краю две закладки: food — еда и taxi — такси. Каждый чек вы вкладываете под закладку его категории. В конце месяца вы складываете чеки под каждой закладкой, а внизу страницы подводите черту и пишете общий итог — строку sum.
В этой теме мы соберём такую тетрадь на Swift. Новых конструкций в ней не будет: программа сложится из того, что вы уже знаете. Она растёт от урока к уроку, но первый блок кода в каждом уроке — целая программа, которую можно запустить отдельно.
Начнём с одного чека. Его можно хранить строкой "food 12.5". Но строка примет и "fod 12.5" с опечаткой, и компилятор её не заметит. А чтобы сложить стоимости, каждую строку пришлось бы заново резать на части.
Удобнее описать чек своими типами, и тогда опечатку поймает компилятор. Модель — это типы, которыми программа описывает предметы из жизни: здесь категорию и чек. Вот модель тетради:
enum Category: String, CaseIterable {
case food, taxi
}
struct Expense: CustomStringConvertible {
let category: Category
let cost: Double
var description: String {
"\(category.rawValue) \(cost)"
}
}
let lunch = Expense(category: .food, cost: 12.5)
print(lunch) // food 12.5Давайте разберём программу по частям:
enum Category— закладки тетради. Список категорий задан заранее, и другой категории в программе не бывает.struct Expense— чек: категория и стоимость под одним именем.- Свойство
descriptionговорит, как чек выглядит при печати. Expense(category: .food, cost: 12.5)заполняет чек почленным инициализатором: метки совпадают с именами свойств.print(lunch)берёт текст изdescriptionи печатаетfood 12.5.
Закладки: перечисление
Категория — выбор из короткого списка, поэтому это перечисление. В объявлении enum Category: String, CaseIterable после двоеточия стоят сырой тип и протокол:
String— тип сырого значения. Вариант без явной строки получает своё имя, поэтому уfoodсырое значение"food"— надпись на закладке.CaseIterable— протокол, который даёт массив всех вариантовallCasesв порядке объявления.
Сырой тип пишут первым, а протоколы — после него через запятую. Если поменять их местами, enum Category: CaseIterable, String, программа не скомпилируется: raw type 'String' must appear first in the enum inheritance clause.
Посмотрим на сырые значения и allCases в деле:
let labels = Category.allCases.map { $0.rawValue }
print(labels.joined(separator: " ")) // food taxi
if let tab = Category(rawValue: "taxi") {
print(tab) // taxi
}
print(Category(rawValue: "bus") == nil) // trueДавайте пройдём по программе:
map { $0.rawValue }превращает каждую закладку в её надпись, аjoinedсклеивает надписи через пробел:food taxi.Category(rawValue: "taxi")находит закладку по надписи. Результат — опционал, иif letего открывает.- Закладки
busв тетради нет, поэтомуCategory(rawValue: "bus")даётnil.
Чек: структура
Чек — это данные из двух граф, поэтому это структура. Свойства объявлены через let: записанный чек не исправляют. Стоимость — Double, потому что у неё бывает дробная часть: 12.5.
Но Double хранит не все дроби точно: print(0.1 + 0.2) печатает 0.30000000000000004. Без потерь он хранит целые числа, половины и четверти, поэтому стоимости этой темы — 12.5, 7 и 4.25 — складываются точно. В настоящих приложениях деньги часто хранят целым числом в копейках.
Что ещё важно знать о Expense:
- Swift сам дал структуре почленный инициализатор, и свой
initписать не пришлось; - число без точки тоже годится: в
Expense(category: .taxi, cost: 7)тип свойства —Double, поэтому7хранится как7.0, так же, как вlet price: Double = 5; descriptionзадаёт текст при печати; если убратьCustomStringConvertibleиз объявления,printпокажет служебную запись вродеExpense(category: main.Category.food, cost: 12.5). ПередCategoryстоит имя программы: послеswiftc main.swiftэтоmain.
Этот вопрос часто задают на собеседовании: почему модель — структура, а не класс? Ответ:
- чек — это данные, а не общая вещь, которую меняют сразу несколько частей программы;
- копия структуры независима: никто не изменит ваш чек у вас за спиной;
- проверка
===и наследование чеку не нужны.
Тетрадь: массив чеков
Чеки складываем в массив — стопку в тетради:
let expenses = [
lunch,
Expense(category: .taxi, cost: 7),
Expense(category: .food, cost: 4.25),
]
print(expenses.count) // 3
for expense in expenses {
print(expense)
}Swift выводит тип массива из его элементов: [Expense]. Запятая после последнего элемента разрешена, и с ней удобно дописывать новые чеки. Цикл печатает три строки, каждый чек через его description:
food 12.5taxi 7.0food 4.25
Здесь картинка с тетрадью неточна. В тетради чек лежит под своей закладкой, а в массиве все чеки идут одной стопкой, в том порядке, в каком их добавили. Закладка — только свойство category каждого чека. Чтобы узнать итог по закладке, стопку придётся перебрать.
Частая ошибка
Категорию передали строкой, как надпись на закладке:
let ride = Expense(category: "taxi", cost: 7) // ✗error: cannot convert value of type 'String' to expected argument type 'Category'Программа не компилируется. У свойства category тип Category, а "taxi" — строка, хоть она и совпадает с сырым значением. Строка сама в вариант не превращается. Напишите вариант, .taxi, или найдите его по надписи через Category(rawValue: "taxi") и откройте опционал.
Главное
- Категория — перечисление
enum Category: String, CaseIterable: сырой тип пишут первым, протокол — через запятую, аallCasesдаёт все варианты по порядку. - Чек — структура
Expenseс неизменяемыми свойствамиcategoryиcost;descriptionизCustomStringConvertibleзадаёт вид при печати. - Чеки хранятся в массиве
[Expense]одной стопкой, в порядке добавления. - Строка не превращается в вариант сама:
Category(rawValue:)ищет вариант по надписи и возвращает опционал.