🍎 Swift

Projeto prático final em Swift: um guia completo

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты учишься в школе кулинарного искусства. Сначала ты учишься резать овощи (переменные), потом жарить яйца (условные операторы), затем печь пироги по рецепту (циклы). Но в конце обучения тебе дают задание: придумать и приготовить собственное трёхблюдное меню для ресторана. Это и есть итоговый практический проект.

Итоговый практический проект — это не просто очередная задача. Это твой шанс собрать воедино всё, что ты узнал: переменные, функции, классы, работу с данными, пользовательский интерфейс. Ты не просто пишешь код, который решает одну маленькую проблему. Ты создаёшь законченное приложение, которое может работать само по себе.

💡 Важно: Итоговый проект — это твой портфолио. Когда ты будешь искать работу или показывать друзьям, что умеешь, ты скажешь: «Вот, я сделал это приложение от начала до конца». Это твой диплом программиста.

В реальной жизни программисты постоянно делают такие проекты. Они не пишут код «просто чтобы было». Они создают приложения для заказа пиццы, игры, программы для учёта расходов. Итоговый проект — это симуляция настоящей работы.

В этой статье мы разберём, как спроектировать и написать такой проект на языке Swift. Мы не будем делать что-то сверхсложное — создадим менеджер заметок (аналог простого блокнота). Но сделаем это правильно: с архитектурой, обработкой ошибок и красивым интерфейсом.

Теория: как это работает изнутри

Любой итоговый проект состоит из трёх слоёв. Представь себе торт:

  • Нижний корж — Модели (Models). Это данные, с которыми работает программа. В нашем случае — заметка: текст, дата создания, заголовок.
  • Средний корж — Логика (Logic). Это мозг программы. Он решает, как сохранять заметки, как их искать, как удалять.
  • Верхний корж — Интерфейс (UI). Это то, что видит пользователь: кнопки, списки, поля ввода.

Почему важно разделять эти слои? Представь, что ты хочешь поменять цвет кнопок в приложении. Если код перемешан, тебе придётся переписывать половину программы. А если слои разделены — ты просто меняешь цвет в слое интерфейса, а логика и данные остаются нетронутыми.

В Swift для этого существует архитектура MVC (Model-View-Controller). Это стандарт для iOS-приложений:

  • Model — данные (например, структура Note)
  • View — экран (Storyboard или SwiftUI)
  • Controller — посредник, который связывает модель и представление

Ещё один важный момент — жизненный цикл приложения. Когда пользователь открывает приложение, запускается функция application(_:didFinishLaunchingWithOptions:). Когда сворачивает — срабатывает applicationDidEnterBackground. В итоговом проекте нужно учитывать эти моменты: например, сохранять данные, когда приложение уходит в фон.

⚠️ Подводный камень: Многие новички пишут весь код в одном файле ViewController.swift. Это работает для маленьких примеров, но для итогового проекта это катастрофа. Через месяц ты сам не разберёшься в своём коде.

Синтаксис в Swift: все формы и варианты

Для итогового проекта тебе понадобятся следующие конструкции Swift. Давай разберём их подробно.

1. Структуры и классы

В Swift есть два способа создавать свои типы данных: struct и class. Для модели данных (заметки) лучше использовать структуру — она легче и безопаснее.

// Структура для заметки
struct Note {
    let id: UUID          // Уникальный идентификатор (неизменяемый)
    var title: String     // Заголовок (можно менять)
    var content: String   // Содержимое
    let createdAt: Date   // Дата создания (неизменяемая)
    
    // Инициализатор — создаёт новую заметку
    init(title: String, content: String) {
        self.id = UUID()           // Генерируем новый UUID
        self.title = title
        self.content = content
        self.createdAt = Date()    // Текущая дата и время
    }
}

2. Протоколы

Протокол — это контракт. Если класс или структура подписываются под протокол, они обязаны реализовать все его методы. Это как список требований.

// Протокол для хранилища заметок
protocol NoteStorage {
    func save(note: Note)           // Сохранить заметку
    func loadAll() -> [Note]        // Загрузить все заметки
    func delete(noteId: UUID)       // Удалить по ID
}

3. Замыкания (Closures)

Замыкания — это функции, которые можно передавать как переменные. Они часто используются для обработки нажатий кнопок или завершения длительных операций.

// Замыкание, которое принимает строку и ничего не возвращает
let printNote: (String) -> Void = { text in
    print("Заметка: (text)")
}

// Вызов замыкания
printNote("Купить молоко")

4. Enum (перечисления)

Перечисления помогают работать с ограниченным набором значений. Например, статус заметки.

enum NoteStatus {
    case active      // Активная
    case archived    // В архиве
    case deleted     // Удалена
}

// Использование
var status = NoteStatus.active
status = .archived  // Можно сокращённо, если тип известен

5. Опциональные типы (Optionals)

В Swift переменная может не иметь значения. Это обозначается знаком ?. Обязательно проверяй опционалы перед использованием.

var maybeNote: Note? = nil  // Пока заметки нет

// Безопасное извлечение
if let note = maybeNote {
    print("Заметка: (note.title)")
} else {
    print("Заметка не найдена")
}

6. Обработка ошибок (try-catch)

Когда что-то может пойти не так (например, не удалось сохранить файл), используй do-catch.

enum NoteError: Error {
    case saveFailed
    case loadFailed
}

func saveNote(_ note: Note) throws {
    // Если что-то пошло не так...
    throw NoteError.saveFailed
}

// Использование
do {
    try saveNote(myNote)
} catch {
    print("Ошибка сохранения: (error)")
}

7. UserDefaults и FileManager

Для хранения данных в простом проекте можно использовать UserDefaults (для маленьких данных) или FileManager (для файлов).

// Сохранение в UserDefaults
let defaults = UserDefaults.standard
defaults.set("Привет, мир!", forKey: "lastNote")

// Чтение
if let saved = defaults.string(forKey: "lastNote") {
    print(saved)
}

Примеры кода — от простого к сложному

Пример 1: Создание модели заметки

// Самая простая модель заметки
struct SimpleNote {
    var title: String
    var content: String
}

// Создаём экземпляр
var myNote = SimpleNote(title: "Список покупок", content: "Молоко, хлеб, яйца")
print("Заголовок: (myNote.title)")  // Выведет: Заголовок: Список покупок

Пример 2: Массив заметок и работа с ним

// Создаём массив заметок
var notes: [SimpleNote] = []

// Добавляем заметки
notes.append(SimpleNote(title: "Идея для приложения", content: "Калькулятор чаевых"))
notes.append(SimpleNote(title: "Рецепт", content: "Паста карбонара"))

// Проходим по массиву и выводим заголовки
for note in notes {
    print("Заметка: (note.title)")
}

// Удаляем первую заметку
notes.remove(at: 0)

Пример 3: Класс-менеджер с протоколом

// Протокол для хранилища
protocol Storage {
    func save(_ note: Note)
    func getAll() -> [Note]
}

// Класс, который реализует протокол
class InMemoryStorage: Storage {
    private var notes: [Note] = []
    
    func save(_ note: Note) {
        notes.append(note)
        print("Сохранено: (note.title)")
    }
    
    func getAll() -> [Note] {
        return notes
    }
}

// Использование
let storage = InMemoryStorage()
let note1 = Note(title: "Встреча", content: "В 15:00 с командой")
storage.save(note1)

Пример 4: Сохранение в файл с помощью Codable

// Делаем модель Codable (может быть закодирована в JSON)
struct CodableNote: Codable {
    let id: UUID
    var title: String
    var content: String
}

// Функция сохранения в файл
func saveToFile(notes: [CodableNote]) {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted  // Красивый вывод
    
    do {
        let data = try encoder.encode(notes)
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentsDirectory.appendingPathComponent("notes.json")
        try data.write(to: fileURL)
        print("Сохранено в файл: (fileURL)")
    } catch {
        print("Ошибка сохранения: (error)")
    }
}

// Создаём тестовые данные
let testNotes = [
    CodableNote(id: UUID(), title: "Тест", content: "Проверка")
]
saveToFile(notes: testNotes)

Пример 5: Загрузка из файла

func loadFromFile() -> [CodableNote] {
    let decoder = JSONDecoder()
    
    do {
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentsDirectory.appendingPathComponent("notes.json")
        
        // Проверяем, существует ли файл
        guard FileManager.default.fileExists(atPath: fileURL.path) else {
            print("Файл не найден")
            return []
        }
        
        let data = try Data(contentsOf: fileURL)
        let notes = try decoder.decode([CodableNote].self, from: data)
        print("Загружено (notes.count) заметок")
        return notes
    } catch {
        print("Ошибка загрузки: (error)")
        return []
    }
}

// Использование
let loadedNotes = loadFromFile()

Пример 6: Простой UI с SwiftUI (экран списка заметок)

import SwiftUI

// Основное представление — список заметок
struct NoteListView: View {
    // @State — это свойство, которое обновляет интерфейс при изменении
    @State private var notes: [Note] = []
    @State private var showAddSheet = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(notes, id: .id) { note in
                    VStack(alignment: .leading) {
                        Text(note.title)
                            .font(.headline)  // Жирный шрифт
                        Text(note.content)
                            .font(.subheadline)  // Мелкий шрифт
                            .foregroundColor(.gray)  // Серый цвет
                    }
                }
                .onDelete { indexSet in
                    notes.remove(atOffsets: indexSet)  // Удаление свайпом
                }
            }
            .navigationTitle("Мои заметки")
            .toolbar {
                Button(action: { showAddSheet = true }) {
                    Image(systemName: "plus")  // Кнопка "+"
                }
            }
            .sheet(isPresented: $showAddSheet) {
                AddNoteView(notes: $notes)  // Экран добавления
            }
        }
    }
}

// Экран добавления новой заметки
struct AddNoteView: View {
    @Binding var notes: [Note]  // Связь с родительским списком
    @State private var title = ""
    @State private var content = ""
    @Environment(.dismiss) var dismiss  // Закрыть экран
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Заголовок", text: $title)
                TextEditor(text: $content)
                    .frame(height: 200)
            }
            .navigationTitle("Новая заметка")
            .toolbar {
                Button("Сохранить") {
                    let newNote = Note(title: title, content: content)
                    notes.append(newNote)
                    dismiss()  // Закрываем экран
                }
            }
        }
    }
}

Пример 7: Полный проект — менеджер заметок с архитектурой MVC

// ========== МОДЕЛЬ (Model) ==========
struct Note: Identifiable, Codable {
    let id: UUID
    var title: String
    var content: String
    let createdAt: Date
    var isFavorite: Bool
    
    init(title: String, content: String) {
        self.id = UUID()
        self.title = title
        self.content = content
        self.createdAt = Date()
        self.isFavorite = false
    }
}

// ========== ЛОГИКА (Controller/Manager) ==========
class NoteManager {
    private var notes: [Note] = []
    private let fileName = "notes.json"
    
    // Загрузка при старте
    init() {
        loadNotes()
    }
    
    // Сохранение заметки
    func addNote(title: String, content: String) {
        let note = Note(title: title, content: content)
        notes.append(note)
        saveNotes()
    }
    
    // Удаление заметки
    func deleteNote(at index: Int) {
        guard index >= 0 && index < notes.count else { return }
        notes.remove(at: index)
        saveNotes()
    }
    
    // Получение всех заметок
    func getAllNotes() -> [Note] {
        return notes
    }
    
    // Переключение избранного
    func toggleFavorite(at index: Int) {
        guard index >= 0 && index < notes.count else { return }
        notes[index].isFavorite.toggle()
        saveNotes()
    }
    
    // Приватный метод сохранения в файл
    private func saveNotes() {
        let encoder = JSONEncoder()
        if let data = try? encoder.encode(notes) {
            let url = getDocumentsDirectory().appendingPathComponent(fileName)
            try? data.write(to: url)
        }
    }
    
    // Приватный метод загрузки из файла
    private func loadNotes() {
        let url = getDocumentsDirectory().appendingPathComponent(fileName)
        if let data = try? Data(contentsOf: url) {
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode([Note].self, from: data) {
                notes = decoded
            }
        }
    }
    
    // Вспомогательный метод для пути к папке документов
    private func getDocumentsDirectory() -> URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }
}

// ========== ПРЕДСТАВЛЕНИЕ (View) ==========
// Используем SwiftUI для интерфейса
import SwiftUI

struct ContentView: View {
    @StateObject private var manager = NoteManager()
    @State private var showAddNote = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(manager.getAllNotes()) { note in
                    HStack {
                        VStack(alignment: .leading) {
                            Text(note.title)
                                .font(.headline)
                            Text(note.content)
                                .font(.caption)
                                .lineLimit(2)
                        }
                        Spacer()
                        // Звёздочка избранного
                        Image(systemName: note.isFavorite ? "star.fill" : "star")
                            .foregroundColor(.yellow)
                            .onTapGesture {
                                if let index = manager.getAllNotes().firstIndex(where: { $0.id == note.id }) {
                                    manager.toggleFavorite(at: index)
                                }
                            }
                    }
                }
                .onDelete { indexSet in
                    for index in indexSet {
                        manager.deleteNote(at: index)
                    }
                }
            }
            .navigationTitle("Заметки")
            .toolbar {
                Button(action: { showAddNote = true }) {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $showAddNote) {
                AddNoteView(manager: manager)
            }
        }
    }
}

struct AddNoteView: View {
    @ObservedObject var manager: NoteManager
    @State private var title = ""
    @State private var content = ""
    @Environment(.dismiss) var dismiss
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Заголовок", text: $title)
                TextEditor(text: $content)
                    .frame(height: 200)
            }
            .navigationTitle("Новая заметка")
            .toolbar {
                Button("Сохранить") {
                    manager.addNote(title: title, content: content)
                    dismiss()
                }
                .disabled(title.isEmpty)  // Нельзя сохранить без заголовка
            }
        }
    }
}

Сравнение с Python: чем похоже и чем отличается

Характеристика Swift Python
Типизация Строгая статическая. Тип указывается явно или выводится компилятором. Нельзя изменить тип переменной после объявления. Динамическая. Переменная может менять тип во время выполнения. Гибко, но容易出现 ошибки.
Опциональные типы Есть встроенная поддержка ? и !. Безопасная работа с nil. Нет опциональных типов. Используется None, но проверка не обязательна.
Производительность Компилируемый язык. Быстрее, чем Python. Подходит для мобильных приложений. Интерпретируемый. Медленнее, но проще для прототипирования.
Парадигма Мультипарадигмальный: ООП, протоколы, функциональное программирование. Мультипарадигмальный: ООП, функциональное, процедурное.
Работа с файлами FileManager, Codable для JSON. Требуется больше кода. Встроенные функции open(), json модуль. Короче.
UI (интерфейс) SwiftUI или UIKit. Сложнее, но мощнее. Tkinter, PyQt. Проще, но менее современно.
Обработка ошибок do-catch, throws. Обязательна обработка. try-except. Необязательна, можно игнорировать.
💡 Совет: Если ты знаешь Python, ты заметишь, что Swift требует больше дисциплины. Но эта дисциплина окупается — твои приложения будут работать быстрее и реже падать.

Типичные ошибки и подводные камни

1. Забываешь про опциональные типы

Самая частая ошибка новичков в Swift — попытка использовать nil без проверки.

var name: String? = nil
print(name.uppercased())  // ОШИБКА! name может быть nil

// Правильно:
if let safeName = name {
    print(safeName.uppercased())
} else {
    print("Имя не задано")
}

2. Не сохраняешь данные при выходе

Если приложение закрылось, а данные не сохранены — они потеряны навсегда.

// В AppDelegate или SceneDelegate
func applicationDidEnterBackground(_ application: UIApplication) {
    // Обязательно сохраняем все данные
    NoteManager.shared.saveAll()
}

3. Путаешь struct и class

Структуры — это типы-значения (копируются). Классы — ссылочные типы (передаются по ссылке).

struct Point {
    var x: Int
}

var p1 = Point(x: 10)
var p2 = p1  // Копия
p2.x = 20
print(p1.x)  // 10 — p1 не изменился

class PointClass {
    var x: Int
    init(x: Int) { self.x = x }
}

var c1 = PointClass(x: 10)
var c2 = c1  // Ссылка на тот же объект
c2.x = 20
print(c1.x)  // 20 — изменилось!

4. Игнорируешь обработку ошибок

Если функция может выбросить ошибку, её нужно обрабатывать. Игнорирование приведёт к падению приложения.

// Плохо:
try? saveData()  // Игнорируем ошибку

// Хорошо:
do {
    try saveData()
} catch {
    print("Не удалось сохранить: (error)")
    // Показываем пользователю сообщение
}

5. Слишком много кода в ViewController

Классическая ошибка — весь проект в одном файле. Разделяй на модели, менеджеры, представления.

Лучшие практики и советы профессионалов

1. Используй Git с самого начала

Создай репозиторий на GitHub и делай коммиты после каждого завершённого блока. Это спасёт тебя, если что-то сломается.

2. Пиши комментарии на русском, но не перебарщивай

Комментируй сложные моменты, но не каждую строку. Код должен быть понятен сам по себе.

3. Тестируй на реальном устройстве

Симулятор — это хорошо, но на реальном iPhone или iPad могут быть нюансы: размер экрана, производительность, жесты.

4. Используй принцип DRY (Don't Repeat Yourself)

Если ты видишь, что один и тот же код повторяется дважды — вынеси его в функцию или метод.

5. Начинай с минимальной версии (MVP)

Сначала сделай приложение, которое просто работает: добавляет и показывает заметки. Потом добавляй фишки: избранное, поиск, тёмную тему.

6. Изучай документацию Apple

Официальная документация Swift и SwiftUI — лучший источник. Не стесняйся читать её на английском.

7. Используй Xcode-шаблоны

При создании нового проекта выбирай шаблон «iOS App» с SwiftUI. Это даст тебе правильную структуру.

Вопросы для самопроверки

  1. Вопрос: Какие три слоя в архитектуре MVC?
    Ответ: Model (данные), View (интерфейс), Controller (логика связи).
  2. Вопрос: Чем отличается struct от class в Swift?
    Ответ: Struct — тип-значение (копируется), class — ссылочный тип (передаётся по ссылке). Struct не поддерживает наследование.
  3. Вопрос: Что такое опциональный тип и как безопасно извлечь значение?
    Ответ: Тип, который может быть nil. Безопасное извлечение через if let или guard let.
  4. Вопрос: Как сохранить данные при закрытии приложения?
    Ответ: Использовать методы жизненного цикла applicationDidEnterBackground или sceneDidEnterBackground.
  5. Вопрос: Что такое протокол в Swift?
    Ответ: Контракт, который определяет набор методов и свойств. Класс или структура, подписанные под протокол, обязаны их реализовать.
  6. Вопрос: Как обработать ошибку в Swift?
    Ответ: Использовать конструкцию do { try ... } catch { ... }.
  7. Вопрос: Для чего нужен Codable?
    Ответ: Для кодирования и декодирования данных в/из JSON (или других форматов). Упрощает сохранение и загрузку.
  8. Вопрос: Что такое @State в SwiftUI?
    Ответ: Свойство, которое автоматически обновляет интерфейс при изменении значения.
  9. Вопрос: Как удалить элемент из массива?
    Ответ: Использовать метод remove(at:) по индексу или removeAll для очистки.
  10. Вопрос: Почему не стоит писать весь код в одном файле?
    Ответ: Это усложняет чтение, тестирование и поддержку. Код становится «спагетти» — трудно найти ошибку.

Итог и что изучить дальше

Ты только что прошёл путь от простой модели данных до полноценного приложения с архитектурой MVC, сохранением в файл и пользовательским интерфейсом. Это огромный шаг. Теперь ты знаешь, как устроен итоговый практический проект на Swift.

Что дальше?

  • Core Data — мощная система для работы с базами данных на iOS. Заменит ручное сохранение в JSON.
  • SwiftUI Advanced — анимации, жесты, работа с камерой и геолокацией.
  • Networking — научись отправлять запросы в интернет (URLSession) и работать с API.
  • Тестирование — пиши unit-тесты для своей логики, чтобы быть уверенным в коде.
  • App Store — изучи, как публиковать приложения. Создай аккаунт разработчика и загрузи свой проект.
🎯 Главный совет: Не останавливайся на одном проекте. Придумай свою идею — приложение для учёта привычек, трекер расходов, мини-игру. И сделай его от начала до конца. Каждый новый проект будет даваться легче.

Ты уже не просто «человек, который учит Swift». Ты — разработчик, который может создать законченный продукт. Гордись этим и продолжай программировать!