SwiftUI практикум — мини-приложение
Дальше: TestFlight и App Store · Swift — о разделе · Первая программа
SwiftUI — список задач от нуля
После первой программы и обзора фреймворков Apple полезно собрать цельное мини-приложение, а не отдельные фрагменты в Playground. Здесь — список задач (to-do): добавление, отметка выполненного, удаление, сохранение между запусками.
Практикум идёт по шагам — от пустого проекта Xcode до приложения, готового к TestFlight.
| Шаг | Что делаем | Результат |
|---|---|---|
| 0 | Проверка Xcode | Симулятор запускается |
| 1 | Новый проект App + SwiftUI | Пустой экран |
| 2 | Модель TaskItem | Структура данных |
| 3 | @State и List | Список на экране |
| 4 | Поле ввода и кнопка | Добавление задачи |
| 5 | NavigationStack | Заголовок и toolbar |
| 6 | @AppStorage | Сохранение между запусками |
| 7 | Preview и отладка | Canvas, типичные ошибки |
| Материал | Зачем |
|---|---|
| Swift — о разделе | Маршрут по языку и экосистеме |
| Первая программа на Swift | Синтаксис, Playground |
| Типы Swift | Struct, optional |
| Жизненный цикл | @main, WindowGroup |
| Xcode | IDE, симулятор, Canvas |
| TestFlight и App Store | Публикация beta |
Навигация по блоку Swift
- База: Swift — о разделе → Первая программа
- UI: Фреймворки Apple
- Вы здесь: SwiftUI практикум
- Следующий шаг: TestFlight и App Store
Установка и первый проект — в статье про Xcode. Для практикума достаточно симулятора iPhone; физическое устройство понадобится позже для TestFlight.
Шаг 0 — проверка окружения
- Установите Xcode из App Store (macOS обязателена для iOS-разработки).
- Откройте Xcode → Settings → Platforms — скачайте iOS Simulator runtime.
- В терминале (опционально):
xcodebuild -version
swift --version
Минимум для практикума — Xcode 15+ и iOS 17 SDK (для NavigationStack и современных API).
Шаг 1 — создать проект
- Xcode → File → New → Project → App.
- Product Name:
TaskMini, Interface: SwiftUI, Language: Swift, Storage: None (добавим сами). - Organization Identifier:
com.example(для учебы). - Запустите на симуляторе iPhone — появится экран "Hello, world!".
Точка входа в TaskMiniApp.swift:
import SwiftUI
@main
struct TaskMiniApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Разбор:
@mainпомечает тип как точку входа приложения.Appописывает сцены (окна) приложения.WindowGroupсоздаёт окно сContentViewвнутри.
Подробнее про @main — в жизненном цикле. Контекст языка — Swift — о разделе.
Шаг 2 — модель задачи
Создайте файл TaskItem.swift (File → New → File → Swift File):
import Foundation
struct TaskItem: Identifiable, Codable, Equatable {
let id: UUID
var title: String
var isDone: Bool
init(title: String) {
self.id = UUID()
self.title = title
self.isDone = false
}
}
Разбор:
Identifiableнужен дляList/ForEach— у каждой строки естьid.Codable— сериализация в JSON для сохранения.Equatable— упрощает тесты и сравнение.UUID()даёт уникальный идентификатор без ручного счётчика.
| Протокол | Роль в этом проекте |
|---|---|
Identifiable | SwiftUI различает элементы списка |
Codable | JSON в UserDefaults |
Equatable | Сравнение и diff |
Шаг 3 — состояние и список
Замените содержимое ContentView.swift:
import SwiftUI
struct ContentView: View {
@State private var tasks: [TaskItem] = [
TaskItem(title: "Изучить @State"),
TaskItem(title: "Собрать List")
]
var body: some View {
List {
ForEach(tasks) { task in
HStack {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
Text(task.title)
.strikethrough(task.isDone)
}
}
}
}
}
#Preview {
ContentView()
}
Разбор:
@State private var tasks— источник правды для списка; при изменении SwiftUI пересобираетbody.ForEach(tasks)обходит массив и создаёт строки.strikethrough(task.isDone)— декларативное оформление: вид зависит от данных.#Preview— Canvas в Xcode для быстрой проверки UI.
Общая идея "данные → UI" — в ООП раздела "Код".
Обычная переменная var tasks без property wrapper не сообщит SwiftUI об изменении. @State хранит значение и триггерит перерисовку.
Шаг 4 — добавление и переключение
Расширьте ContentView:
import SwiftUI
struct ContentView: View {
@State private var tasks: [TaskItem] = [
TaskItem(title: "Изучить @State"),
TaskItem(title: "Собрать List")
]
@State private var newTitle = ""
var body: some View {
VStack {
HStack {
TextField("Новая задача", text: $newTitle)
.textFieldStyle(.roundedBorder)
Button("Добавить") {
addTask()
}
.buttonStyle(.borderedProminent)
.disabled(newTitle.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding()
List {
ForEach($tasks) { $task in
HStack {
Button {
task.isDone.toggle()
} label: {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isDone ? .green : .secondary)
}
.buttonStyle(.plain)
Text(task.title)
.strikethrough(task.isDone)
}
}
.onDelete(perform: deleteTasks)
}
}
}
private func addTask() {
let trimmed = newTitle.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
tasks.append(TaskItem(title: trimmed))
newTitle = ""
}
private func deleteTasks(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
}
}
#Preview {
ContentView()
}
Разбор:
$newTitle— Binding: двусторонняя связь поля ввода с переменной.ForEach($tasks)даёт$task— binding на элемент массива для измененияisDone..onDeleteвключает свайп-удаление в списке..disabled(...)— кнопка неактивна при пустом поле.
Шаг 5 — навигация и заголовок
Оберните содержимое в NavigationStack:
var body: some View {
NavigationStack {
VStack {
// HStack + List как выше
}
.navigationTitle("Задачи")
.toolbar {
EditButton()
ToolbarItem(placement: .primaryAction) {
Button("Очистить выполненные") {
tasks.removeAll { $0.isDone }
}
}
}
}
}
Разбор:
NavigationStack(iOS 16+) — контейнер для заголовка и toolbar.EditButton()включает режим редактирования списка (удаление без свайпа).ToolbarItem— кнопка в navigation bar.
Мобильный контекст — раздел мобильных приложений.
Шаг 6 — сохранение между запусками
Простой вариант — JSON в UserDefaults через @AppStorage:
struct ContentView: View {
@State private var tasks: [TaskItem] = []
@State private var newTitle = ""
@AppStorage("tasks_json") private var tasksData: Data = Data()
var body: some View {
NavigationStack {
// ... UI как выше ...
}
.onAppear { loadTasks() }
.onChange(of: tasks) { _, _ in saveTasks() }
}
private func saveTasks() {
tasksData = (try? JSONEncoder().encode(tasks)) ?? Data()
}
private func loadTasks() {
guard let decoded = try? JSONDecoder().decode([TaskItem].self, from: tasksData),
!decoded.isEmpty else {
tasks = [
TaskItem(title: "Изучить @State"),
TaskItem(title: "Собрать List")
]
return
}
tasks = decoded
}
// addTask, deleteTasks — не забыть saveTasks() или полагаться onChange
}
| Способ хранения | Когда брать |
|---|---|
@AppStorage | Небольшие настройки, прототип |
FileManager + JSON | Больше данных, файлы в Documents |
| SwiftData / Core Data | Связи, запросы, миграции |
Не храните большие blob в UserDefaults — Apple рекомендует файлы или базу. Для десятков задач JSON достаточен.
Шаг 7 — второй экран (NavigationLink)
Упражнение среднего уровня — детали задачи:
NavigationLink {
TaskDetailView(task: $tasks[ index ])
} label: {
Text(task.title)
}
Безопаснее передать binding через helper:
struct TaskDetailView: View {
@Binding var task: TaskItem
var body: some View {
Form {
TextField("Название", text: $task.title)
Toggle("Выполнено", isOn: $task.isDone)
}
.navigationTitle("Детали")
}
}
В List используйте NavigationLink(value: task) с navigationDestination(for: TaskItem.self) — pattern iOS 16+.
Полный файл ContentView — сводка
Соберите все шаги в один файл. Проверьте:
@StateнаtasksиnewTitle;NavigationStack+ title;onAppear/onChangeдля persistence;#Previewкомпилируется.
Запустите на симуляторе: добавьте задачу, отметьте выполненной, перезапустите app — список должен сохраниться.
Проверка и типичные ошибки
| Симптом | Причина | Решение |
|---|---|---|
| Список не обновляется | Изменили массив без @State | Объявите tasks как @State |
ForEach ругается на id | Нет Identifiable | Добавьте id или id: \.self |
| Binding не работает | ForEach(tasks) вместо $tasks | Используйте ForEach($tasks) |
| Preview не компилируется | Нет #Preview | Добавьте блок preview |
| Данные не сохраняются | Нет saveTasks | onChange(of: tasks) |
| Crash при NavigationLink | Index out of range | Binding через id, не index |
| Кириллица в Preview | Encoding | UTF-8 в файле |
Canvas и превью — в Xcode.
Кнопка Debug View Hierarchy в Xcode показывает слои SwiftUI. print(tasks.count) в addTask() — быстрая проверка логики без UI.
Упражнения
- Фильтр — сегмент "Все / Активные / Выполненные" через
@State var filter. - Сортировка — toolbar menu: по дате добавления (добавьте
createdAtв модель). - Пустое состояние — если
tasks.isEmpty, покажитеContentUnavailableView. - Haptic —
UIImpactFeedbackGeneratorпри добавлении (UIKit bridge). - Dark mode — проверьте контраст иконок в Preview → Environment Overrides.
- Unit test — вынесите
addTasklogic вTaskStoreclass, тестируйте без UI.
FAQ
SwiftUI или UIKit для нового проекта?
Для новых apps Apple рекомендует SwiftUI. UIKit нужен для legacy и точечных интеграций.
@State vs @Observable (iOS 17+)?
@State для локального view state. @Observable class — для shared model между экранами. См. Property wrappers.
Почему List внутри VStack "сжимается"?
List в VStack без frame может получить нулевую высоту. Используйте List как корень или .frame(maxHeight: .infinity).
Как тестировать SwiftUI?
ViewInspector, snapshot tests, или логика в отдельном типе + XCTest.
Simulator vs device?
Симулятор достаточен для UI. Camera, push, performance — device.
Production-заметки
| Тема | Рекомендация |
|---|---|
| Accessibility | accessibilityLabel на icon-only buttons |
| Localization | String(localized:) для строк UI |
| Error handling | do/catch при decode JSON |
| Migration | При смене модели — version field в JSON |
| Analytics | Не логируйте title задач без согласия пользователя |
Публикация beta — TestFlight и App Store.
Учебный TaskMini хранит данные локально без шифрования — для production с PII используйте Keychain и privacy manifest. Путь в Store — в TestFlight и App Store.
Куда дальше
- Async/await в Swift — загрузка задач с API.
- Property wrappers —
@Observable,@Bindable(iOS 17+). - TestFlight и App Store — как показать приложение тестерам.
- Интерактивное изучение — быстрые эксперименты в Playground.
- Swift — о разделе — полный маршрут по языку.
Добавьте второй экран с деталями задачи через NavigationLink и передайте binding на title. Это закрепит навигацию и двустороннюю связь данных.
Полный ContentView — итоговый код
Соберите все шаги в один файл. Ниже reference implementation для сверки:
import SwiftUI
struct ContentView: View {
@State private var tasks: [TaskItem] = []
@State private var newTitle = ""
@AppStorage("tasks_json") private var tasksData: Data = Data()
var body: some View {
NavigationStack {
VStack(spacing: 0) {
inputBar
taskList
}
.navigationTitle("Задачи")
.toolbar {
EditButton()
ToolbarItem(placement: .primaryAction) {
Button("Очистить выполненные") {
tasks.removeAll { $0.isDone }
saveTasks()
}
.disabled(!tasks.contains { $0.isDone })
}
}
}
.onAppear { loadTasks() }
}
private var inputBar: some View {
HStack {
TextField("Новая задача", text: $newTitle)
.textFieldStyle(.roundedBorder)
.submitLabel(.done)
.onSubmit { addTask() }
Button("Добавить", action: addTask)
.buttonStyle(.borderedProminent)
.disabled(newTitle.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding()
}
private var taskList: some View {
List {
if tasks.isEmpty {
ContentUnavailableView(
"Нет задач",
systemImage: "checklist",
description: Text("Добавьте первую задачу выше")
)
} else {
ForEach($tasks) { $task in
TaskRowView(task: $task)
}
.onDelete(perform: deleteTasks)
}
}
.listStyle(.insetGrouped)
}
private func addTask() {
let trimmed = newTitle.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
tasks.append(TaskItem(title: trimmed))
newTitle = ""
saveTasks()
}
private func deleteTasks(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
saveTasks()
}
private func saveTasks() {
tasksData = (try? JSONEncoder().encode(tasks)) ?? Data()
}
private func loadTasks() {
guard let decoded = try? JSONDecoder().decode([TaskItem].self, from: tasksData),
!decoded.isEmpty else {
tasks = defaultTasks()
return
}
tasks = decoded
}
private func defaultTasks() -> [TaskItem] {
[
TaskItem(title: "Изучить @State"),
TaskItem(title: "Собрать List")
]
}
}
struct TaskRowView: View {
@Binding var task: TaskItem
var body: some View {
HStack {
Button {
task.isDone.toggle()
} label: {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isDone ? .green : .secondary)
.font(.title3)
}
.buttonStyle(.plain)
.accessibilityLabel(task.isDone ? "Отменить выполнение" : "Отметить выполненным")
Text(task.title)
.strikethrough(task.isDone)
.foregroundStyle(task.isDone ? .secondary : .primary)
}
}
}
#Preview("С задачами") {
ContentView()
}
#Preview("Пустой список") {
ContentView()
}
Разбор итогового файла:
inputBarиtaskList— computed properties для читаемости;ContentUnavailableView— empty state (iOS 17+);onSubmitна TextField — добавление с клавиатуры;TaskRowView— переиспользуемая строка с accessibility;- несколько
#Preview— разные состояния в Canvas.
TaskStore — вынести логику из View
Для тестов и масштабирования вынесите состояние:
import Foundation
@Observable
final class TaskStore {
var tasks: [TaskItem] = []
private let storageKey = "tasks_json"
init() {
load()
}
func add(title: String) {
let trimmed = title.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
tasks.append(TaskItem(title: trimmed))
save()
}
func delete(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
save()
}
func toggleDone(id: UUID) {
guard let index = tasks.firstIndex(where: { $0.id == id }) else { return }
tasks[index].isDone.toggle()
save()
}
func clearCompleted() {
tasks.removeAll { $0.isDone }
save()
}
private func save() {
guard let data = try? JSONEncoder().encode(tasks) else { return }
UserDefaults.standard.set(data, forKey: storageKey)
}
private func load() {
guard let data = UserDefaults.standard.data(forKey: storageKey),
let decoded = try? JSONDecoder().decode([TaskItem].self, from: data),
!decoded.isEmpty else {
tasks = [
TaskItem(title: "Изучить @State"),
TaskItem(title: "Собрать List")
]
return
}
tasks = decoded
}
}
struct ContentView: View {
@State private var store = TaskStore()
@State private var newTitle = ""
var body: some View {
NavigationStack {
// bind to store.tasks
}
}
}
Unit test без UI:
import XCTest
@testable import TaskMini
final class TaskStoreTests: XCTestCase {
func testAddTask() {
let store = TaskStore()
store.add(title: "Test")
XCTAssertEqual(store.tasks.count, 3) // 2 default + 1
}
}
NavigationLink и детальный экран
struct TaskDetailView: View {
@Binding var task: TaskItem
var body: some View {
Form {
Section("Задача") {
TextField("Название", text: $task.title)
Toggle("Выполнено", isOn: $task.isDone)
}
}
.navigationTitle("Детали")
.navigationBarTitleDisplayMode(.inline)
}
}
В списке:
NavigationLink {
TaskDetailView(task: $task)
} label: {
TaskRowView(task: $task)
}
Binding сохраняется — изменения на detail экране отражаются в списке.
Accessibility
| Элемент | Улучшение |
|---|---|
| Icon-only button | .accessibilityLabel |
| List | .accessibilityHint для swipe delete |
| Dynamic Type | .font(.body) вместо фиксированного size |
| VoiceOver | Group related elements .accessibilityElement(children: .combine) |
Проверка: Settings → Accessibility → VoiceOver на симуляторе (⌘F5).
SwiftData alternative (iOS 17+)
Для следующего шага после @AppStorage:
import SwiftData
@Model
final class TaskEntity {
var title: String
var isDone: Bool
var createdAt: Date
init(title: String) {
self.title = title
self.isDone = false
self.createdAt = .now
}
}
В TaskMiniApp:
.modelContainer(for: TaskEntity.self)
SwiftData даёт запросы, связи и миграции — см. Swift — о разделе.
Расширенные упражнения с подсказками
- Due date — добавьте
dueDate: Date?и секцию "Просроченные" красным цветом. - Search —
.searchable(text: $query)фильтруетtasks. - Widget — App Group UserDefaults для Widget Extension (advanced).
- Localization —
String(localized: "tasks_title")+Localizable.xcstrings. - Snapshot test —
assertSnapshotдля ContentView в light/dark. - Haptic —
sensoryFeedback(.success, trigger: tasks.count)iOS 17+.
Расширенный FAQ
Preview зависает?
Infinite loop в onChange — проверьте условие save.
@AppStorage лимит?
~1 MB practical limit; большие данные — файл.
List delete не работает?
Нужен .onDelete на ForEach, не на List напрямую.
Как share checklist?
ShareLink с текстом joined task titles.
iPad layout?
NavigationSplitView — sidebar categories, detail list.
Production — App Store readiness из практикума
| Check | Действие |
|---|---|
| App Icon | Все размеры в Assets |
| Launch Screen | Storyboard или Info.plist |
| Privacy manifest | PrivacyInfo.xcprivacy если SDK требует |
| No debug prints | Удалить print перед Archive |
| Version | CFBundleShortVersionString |
Следующий шаг — TestFlight и App Store.
Анимации и feedback
Button {
withAnimation(.spring(duration: 0.3)) {
task.isDone.toggle()
}
} label: {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity))
Haptic при добавлении (iOS 17+):
.sensoryFeedback(.success, trigger: tasks.count)
Dark Mode и themes
@Environment(\.colorScheme) private var colorScheme
var rowBackground: Color {
colorScheme == .dark ? Color(.systemGray6) : Color(.systemBackground)
}
Preview:
#Preview("Dark") {
ContentView()
.preferredColorScheme(.dark)
}
Поиск через searchable
@State private var searchQuery = ""
var filteredTasks: [TaskItem] {
guard !searchQuery.isEmpty else { return store.tasks }
return store.tasks.filter { $0.title.localizedCaseInsensitiveContains(searchQuery) }
}
// in body:
.searchable(text: $searchQuery, prompt: "Поиск задач")
Drag and drop reorder (iOS 16+)
List {
ForEach($store.tasks) { $task in
TaskRowView(task: $task)
}
.onMove { indices, newOffset in
store.tasks.move(fromOffsets: indices, toOffset: newOffset)
store.save()
}
}
.toolbar {
EditButton() // enables move handles
}
Widget Extension overview (advanced)
- File → New → Target → Widget Extension.
- App Group
group.com.example.TaskMini— shared UserDefaults. - Read same JSON key in Widget
TimelineProvider.
Связь с Swift — о разделе — раздел про extensions.
Instruments — профилирование UI
- Product → Profile (⌘I).
- SwiftUI template — body recomputation count.
- Time Profiler — main thread hangs.
Если body пересчитывается 60+ раз в секунду без действий — вынесите subviews, используйте @Observable granular updates.
Ещё FAQ
Canvas "Failed to build"?
Clean build folder; check target membership of files.
Simulator slow?
Use iPhone SE simulator; reduce animations in Settings.
Git ignore for Xcode?
Use GitHub Swift.gitignore — DerivedData, xcuserdata.
Ещё упражнения (13–16)
- Section headers —
Section("Сегодня")group by date. - Badge — toolbar badge with pending count.
- Pull refresh —
.refreshable { await store.sync() }stub. - App Intents — Siri "Add task" (iOS 16+ App Intents framework).
Пошаговая отладка в Xcode
Список не обновляется
- Поставьте breakpoint в
addTask(). - Убедитесь, что
tasks.appendвызывается. - Проверьте, что
tasksобъявлен как@State. - В View Hierarchy — List пересоздаётся?
Binding errors
Ошибка Cannot assign to property: 'self' is immutable — mutating logic вынесите в метод struct или используйте class @Observable.
JSON decode fails
private func loadTasks() {
guard !tasksData.isEmpty else {
tasks = defaultTasks()
return
}
do {
tasks = try JSONDecoder().decode([TaskItem].self, from: tasksData)
} catch {
tasks = defaultTasks()
}
}
Миграция JSON schema
Храните schemaVersion в корне JSON при эволюции модели. Старые версии декодируйте отдельным типом и маппите на текущий TaskItem.
ContentView на iPad
@Environment(\.horizontalSizeClass) private var sizeClass
var body: some View {
Group {
if sizeClass == .regular {
NavigationSplitView {
Text("Sidebar")
} detail: {
phoneLayout
}
} else {
phoneLayout
}
}
}
Чек-лист перед TestFlight
| # | Item |
|---|---|
| 1 | Release build без crashes |
| 2 | Empty state UI |
| 3 | Accessibility labels |
| 4 | Persistence works |
| 5 | Dark mode OK |
| 6 | App Icon complete |
Дальше — TestFlight и App Store.
Второй проход — расширенный практикум (SwiftUI)
Серия мини-туториалов
Туториал 1 — Observable macro
Команда или API: @Observable class TaskStore.
Детали: shared model iOS 17.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Observable macro |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 2 — Environment
Команда или API: @Environment(\.dismiss).
Детали: close sheet programmatically.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Environment |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 3 — Sheet presentation
Команда или API: .sheet(isPresented:).
Детали: modal add task form.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Sheet presentation |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 4 — Alert confirmation
Команда или API: .alert delete confirm.
Детали: destructive action.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Alert confirmation |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 5 — List edit mode
Команда или API: .environment(\.editMode).
Детали: bulk delete.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить List edit mode |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 6 — Searchable
Команда или API: .searchable(text:).
Детали: filter tasks native.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Searchable |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 7 — SwiftData
Команда или API: @Model class TaskItem.
Детали: replace UserDefaults persistence.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить SwiftData |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 8 — Widget extension
Команда или API: WidgetKit timeline.
Детали: tasks count home screen.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Widget extension |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 9 — App Intents
Команда или API: Siri shortcut add task.
Детали: Shortcuts app integration.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить App Intents |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Туториал 10 — Preview variants
Команда или API: #Preview dark mode.
Детали: multiple preview configs.
// пример шага
console.log('ok');
| Шаг | Проверка |
|---|---|
| 1 | Выполнить Preview variants |
| 2 | Перезапустить dev-сервер |
| 3 | Убедиться в отсутствии ошибок в консоли |
Расширенные упражнения (второй проход)
- Filter segment All Active Done.
Подсказка к упражнению 13: Начните с минимального изменения, затем добавьте тест. Тема: Filter.
- Sort menu by createdAt field.
Подсказка к упражнению 14: Начните с минимального изменения, затем добавьте тест. Тема: Sort.
- ContentUnavailableView empty state.
Подсказка к упражнению 15: Начните с минимального изменения, затем добавьте тест. Тема: ContentUnavailableView.
- Haptic UIImpactFeedbackGenerator add.
Подсказка к упражнению 16: Начните с минимального изменения, затем добавьте тест. Тема: Haptic.
- Dark mode Preview environment.
Подсказка к упражнению 17: Начните с минимального изменения, затем добавьте тест. Тема: Dark.
- Unit test TaskStore logic XCTest.
Подсказка к упражнению 18: Начните с минимального изменения, затем добавьте тест. Тема: Unit.
- NavigationLink value type routing.
Подсказка к упражнению 19: Начните с минимального изменения, затем добавьте тест. Тема: NavigationLink.
- Swipe actions complete delete.
Подсказка к упражнению 20: Начните с минимального изменения, затем добавьте тест. Тема: Swipe.
- ShareLink export tasks json.
Подсказка к упражнению 21: Начните с минимального изменения, затем добавьте тест. Тема: ShareLink.
- Accessibility labels icon buttons.
Подсказка к упражнению 22: Начните с минимального изменения, затем добавьте тест. Тема: Accessibility.
Расширенный FAQ (второй проход)
Observable or State?
Local State; shared Observable class.
List height VStack?
List as root or frame maxHeight infinity.
Preview crash?
Missing environment object inject in Preview.
Codable migration?
Version field migrate JSON decode.
UIKit bridge?
UIViewRepresentable wrap control.
Mac Catalyst?
Same SwiftUI target iPad idiom.
Performance long list?
Lazy stack or pagination fetch.
Keychain secrets?
Not UserDefaults for tokens.
Localization string catalog?
String Catalog Xcode 15+.
TestFlight before UI polish?
Internal beta early feedback ok.
Production — дополнительные рекомендации
| # | Практика | Зачем |
|---|---|---|
| 1 | Privacy | Privacy manifest required APIs declare |
| 2 | Localization | Localization all user strings |
| 3 | Error | Error handling decode failures alert |
| 4 | Analytics | Analytics opt-in if tracking |
| 5 | Keychain | Keychain for sensitive prefs |
| 6 | App | App Store screenshots localized |
| 7 | Crash | Crash symbolication dSYM archive |
| 8 | Performance | Performance Instruments leak check |
Troubleshooting — расширенная таблица
| Симптом | Вероятная причина | Действие |
|---|---|---|
| Сборка падает без текста | Кэш или версия Node | Очистить node_modules, lock-файл, переустановить |
| Тесты flaky | Порядок или timing | Изолировать example, убрать sleep, добавить wait matchers |
| Production 502 | Process не слушает PORT | Проверить env PORT и health endpoint |
| Данные пропали после deploy | In-memory store или migrate | Подключить БД, migrate deploy |
| CORS в браузере | Прямой URL API | Proxy dev или enableCors origin |
| Медленный первый запрос | Cold start DB pool | Warmup health check после deploy |
| Ошибка подписи iOS | Certificate expired | Renew в Developer portal, download profiles |
| Turbo frame blank | Id mismatch | Сверить turbo-frame id в request и response |
| Prisma client outdated | Schema changed | npx prisma generate после migrate |
| Vite blank prod | Неверный base path | Проверить base и URL деплоя |
Пошаговый walkthrough — контрольный список
День 1
- Шаг 1 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 2
- Шаг 1 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 3
- Шаг 1 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 4
- Шаг 1 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 5
- Шаг 1 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 6
- Шаг 1 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 7
- Шаг 1 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
Чек-лист самопроверки перед сдачей практикума
-
Проект создаётся с нуля по статье без пропусков шагов
-
CRUD или эквивалентный сценарий работает end-to-end
-
Есть обработка ошибок валидации или 404
-
Данные переживают перезапуск там, где это требуется темой
-
Написан минимум один автоматический тест или system check
-
Production-секция прочитана и применена к деплою или Docker
-
FAQ просмотрен — типичные ошибки воспроизведены и исправлены
-
Связанные материалы открыты для следующего шага обучения
Дополнение второго прохода — SwiftUI checklist
| Шаг | Проверка |
|---|---|
| Persistence | Перезапуск app сохраняет tasks |
| Navigation | NavigationStack title и toolbar |
| Preview | #Preview компилируется без ошибок |
| A11y | VoiceOver читает кнопки без иконок |
Связанные материалы
| Тема | Материал |
|---|---|
| Язык | Swift — о разделе |
| Xcode | Xcode — IDE Apple |
| Жизненный цикл | Жизненный цикл приложения |
| Публикация | TestFlight и App Store |
| Мобильная разработка | Мобильные приложения |
Дополнительные шаги перед TestFlight
- Проверьте
Info.plist: privacy strings для камеры, геолокации, tracking. - Соберите Archive с Release и
Validate Appв Organizer. - Прогоните UI tests на симуляторе iPhone SE (малый экран).
- Убедитесь, что App Icons и Launch Screen соответствуют HIG.
Дальше: TestFlight и App Store · Xcode.
Итог: подготовка к App Store Connect
После успешной Archive сохраните скриншоты, описание и privacy policy — они понадобятся в TestFlight и App Store.