Перейти к основному содержимому

SwiftUI практикум — мини-приложение

Разработчику

Дальше: TestFlight и App Store · Swift — о разделе · Первая программа


SwiftUI — список задач от нуля

После первой программы и обзора фреймворков Apple полезно собрать цельное мини-приложение, а не отдельные фрагменты в Playground. Здесь — список задач (to-do): добавление, отметка выполненного, удаление, сохранение между запусками.

Практикум идёт по шагам — от пустого проекта Xcode до приложения, готового к TestFlight.

ШагЧто делаемРезультат
0Проверка XcodeСимулятор запускается
1Новый проект App + SwiftUIПустой экран
2Модель TaskItemСтруктура данных
3@State и ListСписок на экране
4Поле ввода и кнопкаДобавление задачи
5NavigationStackЗаголовок и toolbar
6@AppStorageСохранение между запусками
7Preview и отладкаCanvas, типичные ошибки
МатериалЗачем
Swift — о разделеМаршрут по языку и экосистеме
Первая программа на SwiftСинтаксис, Playground
Типы SwiftStruct, optional
Жизненный цикл@main, WindowGroup
XcodeIDE, симулятор, Canvas
TestFlight и App StoreПубликация beta

Навигация по блоку Swift

Xcode и симулятор

Установка и первый проект — в статье про Xcode. Для практикума достаточно симулятора iPhone; физическое устройство понадобится позже для TestFlight.


Шаг 0 — проверка окружения

  1. Установите Xcode из App Store (macOS обязателена для iOS-разработки).
  2. Откройте Xcode → Settings → Platforms — скачайте iOS Simulator runtime.
  3. В терминале (опционально):
xcodebuild -version
swift --version

Минимум для практикума — Xcode 15+ и iOS 17 SDK (для NavigationStack и современных API).


Шаг 1 — создать проект

  1. Xcode → File → New → ProjectApp.
  2. Product Name: TaskMini, Interface: SwiftUI, Language: Swift, Storage: None (добавим сами).
  3. Organization Identifier: com.example (для учебы).
  4. Запустите на симуляторе 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() даёт уникальный идентификатор без ручного счётчика.
ПротоколРоль в этом проекте
IdentifiableSwiftUI различает элементы списка
CodableJSON в 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" — в ООП раздела "Код".

Почему @State

Обычная переменная 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()
}

Разбор:

  • $newTitleBinding: двусторонняя связь поля ввода с переменной.
  • 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Связи, запросы, миграции
UserDefaults и размер

Не храните большие blob в UserDefaults — Apple рекомендует файлы или базу. Для десятков задач JSON достаточен.


Упражнение среднего уровня — детали задачи:

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
Данные не сохраняютсяНет saveTasksonChange(of: tasks)
Crash при NavigationLinkIndex out of rangeBinding через id, не index
Кириллица в PreviewEncodingUTF-8 в файле

Canvas и превью — в Xcode.

Отладка в симуляторе

Кнопка Debug View Hierarchy в Xcode показывает слои SwiftUI. print(tasks.count) в addTask() — быстрая проверка логики без UI.


Упражнения

  1. Фильтр — сегмент "Все / Активные / Выполненные" через @State var filter.
  2. Сортировка — toolbar menu: по дате добавления (добавьте createdAt в модель).
  3. Пустое состояние — если tasks.isEmpty, покажите ContentUnavailableView.
  4. HapticUIImpactFeedbackGenerator при добавлении (UIKit bridge).
  5. Dark mode — проверьте контраст иконок в Preview → Environment Overrides.
  6. Unit test — вынесите addTask logic в TaskStore class, тестируйте без 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-заметки

ТемаРекомендация
AccessibilityaccessibilityLabel на icon-only buttons
LocalizationString(localized:) для строк UI
Error handlingdo/catch при decode JSON
MigrationПри смене модели — version field в JSON
AnalyticsНе логируйте title задач без согласия пользователя

Публикация beta — TestFlight и App Store.

Production

Учебный TaskMini хранит данные локально без шифрования — для production с PII используйте Keychain и privacy manifest. Путь в Store — в TestFlight и App Store.


Куда дальше

  1. Async/await в Swift — загрузка задач с API.
  2. Property wrappers@Observable, @Bindable (iOS 17+).
  3. TestFlight и App Store — как показать приложение тестерам.
  4. Интерактивное изучение — быстрые эксперименты в Playground.
  5. 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
}
}

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
VoiceOverGroup 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 — о разделе.


Расширенные упражнения с подсказками

  1. Due date — добавьте dueDate: Date? и секцию "Просроченные" красным цветом.
  2. Search.searchable(text: $query) фильтрует tasks.
  3. Widget — App Group UserDefaults для Widget Extension (advanced).
  4. LocalizationString(localized: "tasks_title") + Localizable.xcstrings.
  5. Snapshot testassertSnapshot для ContentView в light/dark.
  6. HapticsensoryFeedback(.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 ScreenStoryboard или Info.plist
Privacy manifestPrivacyInfo.xcprivacy если SDK требует
No debug printsУдалить print перед Archive
VersionCFBundleShortVersionString

Следующий шаг — 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)

  1. File → New → Target → Widget Extension.
  2. App Group group.com.example.TaskMini — shared UserDefaults.
  3. Read same JSON key in Widget TimelineProvider.

Связь с Swift — о разделе — раздел про extensions.


Instruments — профилирование UI

  1. Product → Profile (⌘I).
  2. SwiftUI template — body recomputation count.
  3. 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)

  1. Section headersSection("Сегодня") group by date.
  2. Badge — toolbar badge with pending count.
  3. Pull refresh.refreshable { await store.sync() } stub.
  4. App Intents — Siri "Add task" (iOS 16+ App Intents framework).

Пошаговая отладка в Xcode

Список не обновляется

  1. Поставьте breakpoint в addTask().
  2. Убедитесь, что tasks.append вызывается.
  3. Проверьте, что tasks объявлен как @State.
  4. В 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
1Release build без crashes
2Empty state UI
3Accessibility labels
4Persistence works
5Dark mode OK
6App 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Убедиться в отсутствии ошибок в консоли

Расширенные упражнения (второй проход)

  1. Filter segment All Active Done.

Подсказка к упражнению 13: Начните с минимального изменения, затем добавьте тест. Тема: Filter.

  1. Sort menu by createdAt field.

Подсказка к упражнению 14: Начните с минимального изменения, затем добавьте тест. Тема: Sort.

  1. ContentUnavailableView empty state.

Подсказка к упражнению 15: Начните с минимального изменения, затем добавьте тест. Тема: ContentUnavailableView.

  1. Haptic UIImpactFeedbackGenerator add.

Подсказка к упражнению 16: Начните с минимального изменения, затем добавьте тест. Тема: Haptic.

  1. Dark mode Preview environment.

Подсказка к упражнению 17: Начните с минимального изменения, затем добавьте тест. Тема: Dark.

  1. Unit test TaskStore logic XCTest.

Подсказка к упражнению 18: Начните с минимального изменения, затем добавьте тест. Тема: Unit.

  1. NavigationLink value type routing.

Подсказка к упражнению 19: Начните с минимального изменения, затем добавьте тест. Тема: NavigationLink.

  1. Swipe actions complete delete.

Подсказка к упражнению 20: Начните с минимального изменения, затем добавьте тест. Тема: Swipe.

  1. ShareLink export tasks json.

Подсказка к упражнению 21: Начните с минимального изменения, затем добавьте тест. Тема: ShareLink.

  1. 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 — дополнительные рекомендации

#ПрактикаЗачем
1PrivacyPrivacy manifest required APIs declare
2LocalizationLocalization all user strings
3ErrorError handling decode failures alert
4AnalyticsAnalytics opt-in if tracking
5KeychainKeychain for sensitive prefs
6AppApp Store screenshots localized
7CrashCrash symbolication dSYM archive
8PerformancePerformance Instruments leak check

Troubleshooting — расширенная таблица

СимптомВероятная причинаДействие
Сборка падает без текстаКэш или версия NodeОчистить node_modules, lock-файл, переустановить
Тесты flakyПорядок или timingИзолировать example, убрать sleep, добавить wait matchers
Production 502Process не слушает PORTПроверить env PORT и health endpoint
Данные пропали после deployIn-memory store или migrateПодключить БД, migrate deploy
CORS в браузереПрямой URL APIProxy dev или enableCors origin
Медленный первый запросCold start DB poolWarmup health check после deploy
Ошибка подписи iOSCertificate expiredRenew в Developer portal, download profiles
Turbo frame blankId mismatchСверить turbo-frame id в request и response
Prisma client outdatedSchema changednpx prisma generate после migrate
Vite blank prodНеверный base pathПроверить base и URL деплоя

Пошаговый walkthrough — контрольный список

День 1

  1. Шаг 1 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 1: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 2

  1. Шаг 1 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 2: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 3

  1. Шаг 1 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 3: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 4

  1. Шаг 1 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 4: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 5

  1. Шаг 1 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 5: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 6

  1. Шаг 1 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 6: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 7

  1. Шаг 1 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 7: закрепить часть стека SwiftUI. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 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
NavigationNavigationStack title и toolbar
Preview#Preview компилируется без ошибок
A11yVoiceOver читает кнопки без иконок

Связанные материалы

ТемаМатериал
ЯзыкSwift — о разделе
XcodeXcode — IDE Apple
Жизненный циклЖизненный цикл приложения
ПубликацияTestFlight и App Store
Мобильная разработкаМобильные приложения

Дополнительные шаги перед TestFlight

  1. Проверьте Info.plist: privacy strings для камеры, геолокации, tracking.
  2. Соберите Archive с Release и Validate App в Organizer.
  3. Прогоните UI tests на симуляторе iPhone SE (малый экран).
  4. Убедитесь, что App Icons и Launch Screen соответствуют HIG.

Дальше: TestFlight и App Store · Xcode.


Итог: подготовка к App Store Connect

После успешной Archive сохраните скриншоты, описание и privacy policy — они понадобятся в TestFlight и App Store.


Содержание