В этом уроке мы сделаем первый интерактивный экран SwiftUI: текст, кнопку, вертикальный layout и счетчик через @State. Это мини-проект без архитектурной перегрузки. Наша цель — увидеть, как SwiftUI view описывает экран и почему изменение state перерисовывает интерфейс
К концу урока у вас будет экран со счетчиком, который работает в Preview или Simulator
- Что получится в конце
- Что нужно заранее
- View как struct
- Text, VStack и modifiers
- Button и действие
- @State: локальное состояние экрана
- Preview: быстрый цикл проверки
- Домашка: кнопка сброса
- Мини-практика: условный текст
- Частые ошибки и порядок проверки
- Как SwiftUI пересчитывает экран
- Разбиваем body на маленькие части
- Добавляем второе состояние
- Проверка в Preview и Simulator
- Маленькая визуальная доработка без ухода в дизайн
- Домашка: счетчик с ограничением
- Что может быть еще интересно по этой теме
- Что почитать дальше по Swift
Что получится в конце
Файл ContentView.swift:
import SwiftUI
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("SwiftUI counter")
.font(.title)
Text("Clicks: \(count)")
Button("Add") {
count += 1
}
}
.padding()
}
}
#Preview {
ContentView()
}
В Preview появится заголовок, счетчик и кнопка. Нажатие кнопки увеличит число
Что нужно заранее
Нужен Xcode и проект с SwiftUI interface. Если Xcode еще не установлен, сначала откройте Как установить Xcode и создать первый Swift-проект
SwiftUI лучше изучать после базового Swift: let, var, struct, функции, optionals и коллекции. Но первый экран можно собрать уже сейчас, если вы готовы читать код постепенно
View как struct
SwiftUI экран обычно описывается структурой:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}
ContentView соответствует протоколу View. body возвращает описание интерфейса
Это не imperative-код в стиле «создай label, поставь координаты, добавь на экран». SwiftUI описывает, каким должен быть UI для текущего состояния
Text, VStack и modifiers
Text показывает текст:
Text("SwiftUI counter")
VStack складывает элементы вертикально:
VStack(spacing: 16) {
Text("Title")
Text("Subtitle")
}
Modifiers настраивают view:
Text("SwiftUI counter")
.font(.title)
Они читаются цепочкой сверху вниз. Не надо сразу знать все modifiers. Для первого экрана хватит font, padding, foregroundStyle, background
Button и действие
Кнопка:
Button("Add") {
count += 1
}
Первый аргумент — текст кнопки. Блок в фигурных скобках — действие по нажатию
Если внутри действия ничего не происходит, кнопка видна, но бесполезна. В нашем примере действие меняет count
@State: локальное состояние экрана
Строка:
@State private var count = 0
говорит SwiftUI: это локальное состояние view. Когда count меняется, SwiftUI пересчитывает body, и экран показывает новое значение
private показывает, что состояние принадлежит этому view. Другие views не должны менять его напрямую. Когда понадобится передать состояние в дочерний view, появится @Binding
Не используйте @State для всего подряд. Он хорош для локального состояния небольшого view: счетчик, переключатель, выбранный фильтр, текст поля
Preview: быстрый цикл проверки
#Preview показывает view в Xcode canvas:
#Preview {
ContentView()
}
Если Preview не обновляется, сначала соберите проект. Если сборка красная, Preview не обязан работать
Preview — ваш быстрый стенд. Меняете текст, spacing, padding, состояние — сразу смотрите результат без полного запуска приложения
Домашка: кнопка сброса
Добавьте вторую кнопку:
Button("Reset") {
count = 0
}
Проверьте:
- Add увеличивает счетчик
- Reset возвращает ноль
- Preview не падает
Дополнительная задача: если count == 0, показывать текст "No clicks yet", иначе "Clicks: \(count)"
Мини-практика: условный текст
Добавьте:
if count == 0 {
Text("No clicks yet")
} else {
Text("Clicks: \(count)")
}
SwiftUI позволяет писать условия внутри body. Это удобно, но не превращайте body в огромную функцию. Когда экран растет, выносите части в отдельные views
Частые ошибки и порядок проверки
Cannot assign to property: self is immutable Скорее всего, вы пытаетесь менять обычное свойство view без @State
Preview не показывает изменения Сначала проверьте сборку проекта. Потом обновите Preview
Положили @State в неправильное место @State должен быть свойством view, а не локальной переменной внутри body
Сразу строите сложную архитектуру Для первого экрана не нужен MVVM. Сначала поймите Text, Button, VStack и @State
Как SwiftUI пересчитывает экран
SwiftUI view описывает, как экран должен выглядеть для текущего состояния. Когда меняется @State, SwiftUI заново вычисляет body и сравнивает новое описание UI с прежним. Вам не нужно вручную искать label и менять ему текст
В счетчике это видно прямо:
@State private var count = 0
var body: some View {
Text("Count: \(count)")
Button("Add") {
count += 1
}
}
Кнопка меняет count, а Text показывает новое значение. Между ними нет ручного вызова reload, refresh или setText
Это главная привычка SwiftUI: вы описываете зависимость интерфейса от состояния. Если состояние верное, UI сам приходит к нужному виду
Разбиваем body на маленькие части
Когда экран растет, body быстро становится шумным. Первый безопасный шаг — вынести куски в computed properties:
var titleView: some View {
Text("SwiftUI counter")
.font(.title)
}
var controls: some View {
Button("Add") {
count += 1
}
}
Потом собрать:
var body: some View {
VStack(spacing: 16) {
titleView
Text("Count: \(count)")
controls
}
.padding()
}
Так вы не вводите архитектуру раньше времени, но уже держите экран читаемым. Это хороший промежуточный прием перед отдельными child views и @Binding
Добавляем второе состояние
Попробуйте добавить имя:
@State private var name = "Dinar"
И кнопку:
Button("Change name") {
name = name == "Dinar" ? "Swift learner" : "Dinar"
}
Покажите текст:
Text("Student: \(name)")
Теперь экран зависит от двух состояний: count и name. Это все еще нормально для первого урока. Но если состояний становится много, появляется смысл выносить части экрана и думать о модели данных
Проверка в Preview и Simulator
Preview удобен для быстрой проверки layout, но иногда он зависает или показывает старое состояние. Если сомневаетесь, запустите приложение в Simulator. Для учебного экрана правило простое:
- Preview показывает верстку
- Simulator проверяет реальный запуск
- Console помогает читать ошибки
Если Preview не обновляется, не переписывайте код наугад. Сначала сделайте clean build или перезапустите preview. Если Simulator собирает проект нормально, проблема может быть именно в preview canvas
Маленькая визуальная доработка без ухода в дизайн
Чтобы экран выглядел не как голая проверка кнопки, добавьте пару модификаторов:
Text("Count: \(count)")
.font(.largeTitle)
.fontWeight(.semibold)
Button("Add") {
count += 1
}
.buttonStyle(.borderedProminent)
Модификаторы в SwiftUI читаются сверху вниз: берется view и к нему последовательно применяются настройки. Это не CSS и не XML-разметка, но идея похожа: вы описываете внешний вид декларативно
Не пытайтесь сразу сделать идеальный интерфейс. Для первого экрана достаточно, чтобы текст был читаемым, кнопка была заметной, а изменение состояния проверялось без догадок
Домашка: счетчик с ограничением
Сделайте так, чтобы счетчик не уходил ниже нуля:
Button("Minus") {
if count > 0 {
count -= 1
}
}
Дополнительная задача: если count == 0, показывайте текст Minimum reached. Так вы потренируете условный UI и увидите, как SwiftUI реагирует на изменение состояния
Еще одна проверка: добавьте кнопку Reset, которая возвращает счетчик в ноль. Если текст, условное сообщение и кнопки синхронно обновляются после каждого нажатия, базовая модель @State действительно понятна
Что может быть еще интересно по этой теме
SwiftUI заменяет UIKit? SwiftUI активно используется для новых интерфейсов, но UIKit остается в большом количестве проектов. Новичку можно начинать со SwiftUI
Почему view является struct? SwiftUI работает с декларативным описанием UI и состоянием. Struct хорошо подходит для описания значения view
Когда нужен @Binding? Когда состояние хранится в родительском view, но дочерний view должен его менять
Что дальше после первого экрана? Разберите @Binding, чтобы счетчик или переключатель можно было вынести в отдельный компонент
Что почитать дальше по Swift
- Массивы, словари и циклы в Swift
- @Binding и передача данных между SwiftUI views
- async await в Swift: первый сетевой запрос
- Optionals в Swift: ?, ! и безопасное извлечение



