SwiftUI
Декларативный UI-фреймворк — состояние, связывания и композиция представлений.
18 вопросов
JuniorТеорияОчень частоВ чём разница между @State и Binding в SwiftUI?
В чём разница между @State и Binding в SwiftUI?
@State объявляет изменяемое локальное состояние, которым владеет представление; его изменение перерисовывает body. Binding ($state) — двусторонняя ссылка на него: дочерний контрол вроде TextField читает и пишет значение родителя, не владея им.
Типичные ошибки
- ✗Забывают префикс
$при передаче состояния как binding в дочернее представление - ✗Считают
@Stateобщим для экземпляров, а не отдельным для каждого - ✗Хранят ссылочные модели в
@Stateвместо@StateObject
Уточняющие вопросы
- →Почему SwiftUI перерисовывает body при изменении значения
@State? - →Когда вы возьмёте
@StateObjectвместо@State?
MiddleДебаггингОчень частоonAppear срабатывает дважды — или не срабатывает, когда ждёшь. Чем это отличается от жизненного цикла UIKit?
onAppear срабатывает дважды — или не срабатывает, когда ждёшь. Чем это отличается от жизненного цикла UIKit?
onAppear следует за структурной идентичностью представления, а не за циклом контроллера, и срабатывает при каждом входе в иерархию. Переустановленное представление вызывает его снова; так и не вставленное — не вызывает. Это не viewDidAppear «раз на экран».
Типичные ошибки
- ✗Считают
onAppearэквивалентомviewDidAppear«раз на экран» - ✗Кладут чувствительную к повторам настройку в
onAppearбез защиты - ✗Ждут, что
onAppearсработает для представления, которое так и не вставлено
Уточняющие вопросы
- →Почему переопределённое по идентичности представление снова вызывает
onAppear? - →Куда поместить логику загрузки, чтобы повторная вставка её не повторяла?
MiddleПроизводительностьОчень часто.task против .onAppear против .onChange — где запускать асинхронную загрузку и кто её отменяет?
.task против .onAppear против .onChange — где запускать асинхронную загрузку и кто её отменяет?
Запускайте в .task: он стартует при появлении и отменяется автоматически при исчезновении. .onAppear синхронный и без отмены, поэтому Task оттуда течёт или перезапускается. .task(id:) перезагружает по изменению, сохраняя авто-отмену.
Типичные ошибки
- ✗Запускают неотменяемый
Taskвнутри.onAppear - ✗Считают, что
.onChangeсрабатывает при каждом рендере, а не при изменении значения - ✗Не используют
.task(id:), чтобы перезагружать с сохранением авто-отмены
Уточняющие вопросы
- →Почему Task, запущенный в
.task, отменяется при исчезновении представления? - →Как
.task(id:)перезагружает, не утекая предыдущим Task?
JuniorТеорияЧастоКак представление подписывается на ObservableObject и кто владеет — @StateObject, @ObservedObject или @EnvironmentObject?
Как представление подписывается на ObservableObject и кто владеет — @StateObject, @ObservedObject или @EnvironmentObject?
Представление наблюдает за ObservableObject, храня его в @StateObject, @ObservedObject или @EnvironmentObject. Поля @Published перерисовывают подписанные представления при изменении. @StateObject владеет им; два остальных ссылаются на объект из другого места.
Типичные ошибки
- ✗Думают, что нужно вручную подписываться через
sink, а не просто объявить обёртку - ✗Считают, что
@ObservedObjectвладеет объектом и держит его живым - ✗Путают, какая обёртка владеет объектом, а какая ссылается на внешний
Уточняющие вопросы
- →Что на самом деле делает пометка свойства
@Publishedдля наблюдающих представлений? - →Какая из трёх обёрток держит объект живым и почему это важно?
JuniorТеорияЧастоЧто означает «представление — это описание, а не объект» для onAppear и onDisappear?
Что означает «представление — это описание, а не объект» для onAppear и onDisappear?
Значение представления — лишь описание, читаемое SwiftUI для дерева отрисовки; структура создаётся и выбрасывается постоянно, без жизненного цикла. onAppear/onDisappear срабатывают при входе узла в иерархию или выходе, а не при создании структуры.
Типичные ошибки
- ✗Приравнивают
initструктуры к появлению представления на экране - ✗Считают структуры представлений долгоживущими объектами с жизненным циклом
- ✗Ждут, что
onAppearвыполнится раз на приложение, а не на каждую вставку
Уточняющие вопросы
- →Почему одна и та же структура представления может создаваться много раз, не появляясь?
- →Когда именно срабатывает
onDisappearотносительно дерева отрисовки?
JuniorТеорияЧастоПочему представления SwiftUI — это структуры, что означает some View и как часто пересчитывается body?
Почему представления SwiftUI — это структуры, что означает some View и как часто пересчитывается body?
Представления — лёгкие структуры, значимые типы, хранящие описание UI, а не отрисованные объекты, поэтому SwiftUI создаёт и выбрасывает их постоянно. some View — непрозрачный возвращаемый тип, один конкретный View. body пересчитывается при изменении состояния или входов.
Типичные ошибки
- ✗Считают представления долгоживущими объектами, а не временными значимыми типами
- ✗Читают
some Viewкакany View(экзистенциал), а не как один конкретный тип - ✗Полагают, что пересчёт
bodyдорог и его нужно избегать
Уточняющие вопросы
- →Почему SwiftUI дёшево пересчитывать
bodyмного раз? - →Чем
some Viewотличается отany View?
MiddleДебаггингЧастоИсправьте «@EnvironmentObject may be missing as an ancestor» — и чем отличается @Environment?
Исправьте «@EnvironmentObject may be missing as an ancestor» — и чем отличается @Environment?
Представление прочитало @EnvironmentObject, который не внедрил ни один предок через .environmentObject(_:). Внедрите его на нужный sheet или назначение навигации — они не наследуют окружение вызвавшего. @Environment иной — значение по ключу с умолчанием.
Типичные ошибки
- ✗Не внедряют объект в окружение sheet или назначения навигации
- ✗Путают
@EnvironmentObject(внедрённый объект) с@Environment(значение по ключу) - ✗Делают свойство опциональным, чтобы заглушить падение, вместо внедрения выше
Уточняющие вопросы
- →Почему представленный sheet не наследует environment object вызвавшего его экрана?
- →Почему
@Environmentне может упасть из-за отсутствующего значения, как@EnvironmentObject?
MiddleДебаггингЧастоИсправьте предупреждение времени выполнения «Modifying state during view update»
Исправьте предупреждение времени выполнения «Modifying state during view update»
Оно возникает, когда body меняет наблюдаемое состояние, пока SwiftUI ещё вычисляет этот body — присваивание @State во время вычисления. Вынесите изменение из прохода отрисовки в .onAppear, .task, .onChange или действие пользователя.
Типичные ошибки
- ✗Присваивают значения
@State/@Publishedпрямо внутриbody - ✗Думают, что
DispatchQueue.main.asyncвокруг записи чинит первопричину - ✗Считают предупреждение безобидным, а не реальной ошибкой реэнтерабельности
Уточняющие вопросы
- →Почему изменение наблюдаемого состояния во время вычисления
bodyне определено? - →Каким модификатором вы бы запустили изменение уже после появления представления?
MiddleТеорияЧастоЧто ломается, когда представление само создаёт свою модель через @ObservedObject вместо @StateObject?
Что ломается, когда представление само создаёт свою модель через @ObservedObject вместо @StateObject?
@StateObject делает представление владельцем модели — SwiftUI создаёт её один раз и держит живой между перерисовками. @ObservedObject лишь наблюдает за внешним объектом, поэтому создание модели здесь пересоздаёт её при рендере, сбрасывая состояние.
Типичные ошибки
- ✗Считают, что
@ObservedObjectвладеет и хранит модель, как@StateObject - ✗Создают модель прямо внутри представления, помеченную
@ObservedObject - ✗Думают, что выбор косметический, ведь оба компилируются и часто выглядят нормально
Уточняющие вопросы
- →Почему
@StateObjectпереживает перерисовку родителя, а созданный локально@ObservedObject— нет? - →Когда
@ObservedObject— правильный выбор для модели?
MiddleКодЧастоПерепишите UIKit-форму ввода имени как SwiftUI-представление
Перепишите UIKit-форму ввода имени как SwiftUI-представление
Замените контроллер на View с @State для имени и приветствия. TextField("Name", text: $name) связывает ввод, Button задаёт приветствие, а if !greeting.isEmpty { Text(greeting) } заменяет isHidden. VStack с padding заменяет Auto Layout.
Типичные ошибки
- ✗Берут
UIViewControllerRepresentableвместо нативного SwiftUI-представления - ✗Прячут элемент флагом вместо условного включения view (
if ...) - ✗Сохраняют ручные ограничения Auto Layout вместо декларативной раскладки на стеках
Уточняющие вопросы
- →Почему
if !greeting.isEmptyудаляет view, а не просто скрывает его? - →Как
$nameдержитTextFieldи модель синхронными без target-action?
MiddleТеорияИногдаКак макрос @Observable (iOS 17+) меняет то, какие представления инвалидируются, по сравнению с ObservableObject?
Как макрос @Observable (iOS 17+) меняет то, какие представления инвалидируются, по сравнению с ObservableObject?
ObservableObject перерисовывает каждое наблюдающее представление при изменении любого поля @Published. Макрос @Observable (iOS 17+) отслеживает чтение свойств, поэтому инвалидируется лишь при изменении прочитанного. Сочетайте его с @State.
Типичные ошибки
- ✗Думают, что
@Observableлишь убирает@Published, сохраняя инвалидизацию всего объекта - ✗Сочетают
@Observableс@StateObjectвместо@State - ✗Считают
@Observableобёрткой свойства, а не макросом
Уточняющие вопросы
- →Почему отслеживание чтения по свойствам сокращает число инвалидаций представлений?
- →Почему
@Observableсочетают с@State, а не с@StateObject?
MiddleДебаггингИногдаСтроки List сбрасывают состояние после переупорядочивания, потому что id — это индекс массива
Строки List сбрасывают состояние после переупорядочивания, потому что id — это индекс массива
Привязка строк к индексу массива связывает идентичность с позицией: после переупорядочивания слот 0 остаётся тем же представлением со старым состоянием. Дайте элементу стабильный Identifiable id, чтобы идентичность шла за данными, а не за слотом.
Типичные ошибки
- ✗Используют индекс массива (смещение) как идентичность строки
- ✗Считают, что SwiftUI отслеживает строки по содержимому, а не по заданному
id - ✗Добавляют
.id(UUID()), чтобы форсировать пересборку, вместо стабильной идентичности
Уточняющие вопросы
- →В чём разница между структурной идентичностью и явной идентичностью через
.id()? - →Почему стабильный
Identifiableid сохраняет@Stateстроки при перемещении?
MiddleКодИногдаНапишите свой контейнер-представление, принимающий @ViewBuilder-замыкание
Напишите свой контейнер-представление, принимающий @ViewBuilder-замыкание
Сделайте обобщённую struct, хранящую content: Content (Content: View), и пометьте замыкание инициализатора @ViewBuilder content: () -> Content. Сохраните результат и разместите content в раскладке. Атрибут даёт передать несколько представлений замыканием.
Типичные ошибки
- ✗Объявляют замыкание как
[AnyView]вместо обобщённого@ViewBuilder-замыкания - ✗Забывают
@ViewBuilder, из-за чего компилируется лишь одно дочернее представление - ✗Делают замыкание содержимого
@escapingи вызывают его заново при каждом рендере
Уточняющие вопросы
- →Почему
@ViewBuilderпозволяет вызывающему передать несколько представлений без запятых и массива? - →Какой конкретный тип производит билдер для нескольких детей?
SeniorПроизводительностьИногдаList из 10 000 строк дёргается. Что SwiftUI создаёт лениво и что ломает эту ленивость?
List из 10 000 строк дёргается. Что SwiftUI создаёт лениво и что ломает эту ленивость?
List и LazyVStack строят только строки рядом с видимой областью и переиспользуют их при прокрутке, поэтому огромная коллекция дешева. Ленивость ломается, когда строки стирают до AnyView, кладут в обычный неленивый VStack или дают им дорогой body.
Типичные ошибки
- ✗Оборачивают строки в
AnyView, ломая переиспользование и сравнение - ✗Используют обычный
VStack/ScrollViewвместоList/LazyVStack - ✗Делают дорогую работу (форматирование, декодирование) внутри
bodyстроки
Уточняющие вопросы
- →Почему
AnyViewмешает SwiftUI переиспользовать строку? - →Чем
LazyVStackотличается от обычногоVStackдля больших данных?
SeniorПроизводительностьИногдаФорма перерисовывает весь экран при каждом нажатии клавиши, потому что наблюдается вся модель целиком. Как сузить инвалидизацию?
Форма перерисовывает весь экран при каждом нажатии клавиши, потому что наблюдается вся модель целиком. Как сузить инвалидизацию?
Перейдите на макрос @Observable, чтобы SwiftUI отслеживал чтение свойств и перерисовывал лишь изменившееся. Разбейте модель на мелкие наблюдаемые части и вынесите каждое поле в подпредставление — его body станет единицей инвалидизации.
Типичные ошибки
- ✗Держат одну огромную
ObservableObject, наблюдаемую целиком из одного представления - ✗Хватаются за
debounceвместо сужения того, что наблюдается - ✗Считают, что кэширование через
AnyViewуменьшает число перерисовок
Уточняющие вопросы
- →Почему вынос поля в отдельное подпредставление ограничивает перерисовку этим полем?
- →Как
@Observableизбегает инвалидизации представлений, не читавших свойство?
SeniorДизайнИногдаНужно встроить UIKit-карту MKMapView в экран SwiftUI. Аннотации управляются состоянием SwiftUI, а когда пользователь двигает карту или тапает по пину, окружающее представление SwiftUI должно обновиться (например, панель деталей выбранного места). Опишите, как связать карту через протокол интеропа UIViewRepresentable: что делают makeUIView и updateUIView, зачем нужен Coordinator и как держать состояние синхронным в обе стороны — состояние SwiftUI в карту и колбэки делегата карты (смена региона, тапы по пинам) обратно в SwiftUI — без циклов обратной связи. Отметьте владение и время жизни MKMapView и Coordinator.
Нужно встроить UIKit-карту MKMapView в экран SwiftUI. Аннотации управляются состоянием SwiftUI, а когда пользователь двигает карту или тапает по пину, окружающее представление SwiftUI должно обновиться (например, панель деталей выбранного места). Опишите, как связать карту через протокол интеропа UIViewRepresentable: что делают makeUIView и updateUIView, зачем нужен Coordinator и как держать состояние синхронным в обе стороны — состояние SwiftUI в карту и колбэки делегата карты (смена региона, тапы по пинам) обратно в SwiftUI — без циклов обратной связи. Отметьте владение и время жизни MKMapView и Coordinator.
Оберните карту в UIViewRepresentable: makeUIView создаёт MKMapView один раз; updateUIView проталкивает в неё новое состояние. Coordinator держит её делегат и передаёт колбэки — смену региона, тапы по пинам — обратно в SwiftUI через @Binding, защищая обратную запись от зацикливания.
Типичные ошибки
- ✗Пересоздают
MKMapViewвupdateUIViewвместо однократного создания вmakeUIView - ✗Опускают Coordinator и теряют колбэки делегата
- ✗Не защищают обратную запись, создавая цикл между состоянием и делегатом
Уточняющие вопросы
- →Как не дать программной смене региона снова вызвать делегат карты?
- →Кто владеет
MKMapViewи Coordinator и когда они освобождаются?
JuniorТеорияРедкоКак result builder @ViewBuilder превращает представления в body в TupleView и в чём смысл ограничения на 10?
Как result builder @ViewBuilder превращает представления в body в TupleView и в чём смысл ограничения на 10?
@ViewBuilder — result builder, объединяющий представления из body в одно составное значение — для нескольких детей это TupleView. Цикла во время выполнения нет; вызов синтезирует компилятор. В ранних SwiftUI перегрузки были лишь до десяти детей.
Типичные ошибки
- ✗Думают, что
@ViewBuilderстроит массив во время выполнения, а не статический композит - ✗Считают ограничение на десять рекомендацией по стилю, а не ограничением компилятора
- ✗Не знают, что несколько детей становятся
TupleView
Уточняющие вопросы
- →Почему перечисление двух представлений в
bodyдаётTupleView? - →Как обёртка детей в
Groupобходит ограничение на десять представлений?