Вопросы по языку Swift, управлению памятью (ARC), многопоточности (GCD, Actors), SwiftUI и UIKit.
* **Тип данных (Value vs Reference)**: `Struct` — это тип значения (Value Type). При передаче структуры в функцию или присвоении переменной создается ее полная копия (используется механизм Copy-on-Write для оптимизации). `Class` — это ссылочный тип (Reference Type). При передаче класса копируется ссылка на один и тот же объект в памяти. * **Хранение в памяти**: Структуры обычно хранятся в стеке (Stack), что обеспечивает быструю аллокацию. Экземпляры классов хранятся в куче (Heap), а в стеке хранятся лишь ссылки на них. * **Наследование**: Классы поддерживают наследование, полиморфизм и деинициализаторы (`deinit`). Структуры наследование не поддерживают (но могут реализовывать протоколы). * **Мутабельность**: Чтобы изменить свойства структуры внутри ее метода, этот метод должен быть помечен ключевым словом `mutating`. Свойства класса можно изменять, даже если ссылка на класс объявлена как константа `let`.
ARC автоматически управляет временем жизни объектов ссылочного типа (классов), подсчитывая количество активных сильных ссылок на них. * **Принцип работы**: Когда создается экземпляр класса, ARC выделяет память в куче и устанавливает счетчик ссылок в 1. При создании новых сильных ссылок счетчик увеличивается. Когда ссылка выходит из области видимости или приравнивается к `nil`, счетчик уменьшается. При падении счетчика до 0 память освобождается. * **Утечки памяти (Memory Leaks)**: Возникают при циклической сильной ссылке (Strong Reference Cycle), когда два объекта ссылаются друг на друга сильными ссылками. Для предотвращения циклов используются слабые (`weak`) и бесхозные (`unowned`) ссылки.
Оба ключевых слова используются для разрыва сильных циклических ссылок, не увеличивая счетчик сильных ссылок: * **`weak`**: Ссылка всегда должна быть опциональной переменной (`var` с типом `?`). Когда объект, на который указывает weak-ссылка, уничтожается, ARC автоматически зануляет эту ссылку (устанавливает в `nil`). Безопасна в использовании. * **`unowned`**: Ссылка не увеличивает счетчик, но ожидает, что объект будет существовать все время, пока существует сама ссылка. Она не может быть `nil` и объявляется как неопциональная константа или переменная. Если обратиться к unowned-ссылке после того, как объект был удален, приложение завершится аварийно (crash).
Copy-on-Write — это оптимизация управления памятью для типов значений (массивы, словари, строки). * **Как работает**: При копировании структуры (например, присвоении массива `array2 = array1`) реального копирования элементов в памяти не происходит. Обе переменные указывают на одну и ту же область памяти. Копирование данных происходит только в тот момент, когда одна из переменных пытается изменить данные (например, вызвать `append`). * **Реализация**: CoW встроен во все стандартные коллекции Swift. Для кастомных структур его можно реализовать вручную, используя проверку уникальности ссылки `isKnownUniquelyReferenced` на приватный ссылочный контейнер внутри структуры.
* **Non-escaping замыкание** (по умолчанию): Замыкание передается в функцию, выполняется внутри нее, и функция завершает свою работу. Время жизни замыкания ограничено выполнением функции. Оно не может пережить область видимости функции. * **Escaping замыкание** (помечается `@escaping`): Замыкание может быть выполнено после завершения функции (например, сохранено в переменную класса, выполнено асинхронно в блоке GCD или сетевом запросе). Это критично для ARC, так как escaping-замыкание может захватывать ссылки (`self`), создавая циклы утечки памяти, если не использовать список захвата `[weak self]`.
`actor` — это новый тип данных в Swift (наряду с struct и class), предназначенный для защиты состояния от гонок за данные (data races) в многопоточной среде. * **Свойства**: Акторы являются ссылочными типами (подобно классам), но они не поддерживают наследование. * **Защита данных**: Актор изолирует свои изменяемые свойства. Доступ к методам и свойствам актора извне разрешен только асинхронно с использованием ключевого слова `await`. Это гарантирует, что в любой момент времени только один поток может выполнять код внутри актора, предотвращая конкурентный доступ к изменяемому состоянию.
* **GCD**: Классический фреймворк многопоточности на основе очередей задач (`DispatchQueue.global().async`). Работает на колбэках (closures), что часто приводит к сложному вложенному коду (Callback Hell) и трудностям в обработке ошибок. * **async/await** (Swift Concurrency): Современная модель кооперативного пула потоков. Позволяет писать асинхронный код последовательно, как если бы он был синхронным. Упрощает чтение, разгружает стек от лишних аллокаций замыканий и интегрируется с механизмами отмены задач (`Task.cancel()`).
`@MainActor` — это глобальный актор, который гарантирует выполнение помеченного им кода (класса, метода или свойства) исключительно в главном потоке (Main Thread). * **Применение**: Используется для работы с UI. Например, если класс ViewModel помечен `@MainActor`, все его методы, обновляющие свойства для SwiftUI-представлений, будут гарантированно выполняться в главном потоке, предотвращая падения из-за фонового обновления интерфейса.
Протоколы в Swift похожи на интерфейсы в других языках, но обладают расширенными возможностями: * **Протокольно-ориентированное программирование (POP)**: С помощью расширений (`extension`) протоколу можно задать реализацию методов по умолчанию. Любой тип, реализующий этот протокол, автоматически получит это поведение. * **Ассоциированные типы (Associated Types)**: Протоколы поддерживают дженерики через ассоциированные типы (`associatedtype`), что позволяет создавать гибкие контракты для коллекций и контейнеров.
* **`Any`**: Может представлять экземпляр любого типа вообще, включая классы, структуры, перечисления (enum), функции и замыкания. * **`AnyObject`**: Может представлять экземпляр любого класса (ссылочного типа). Часто используется как ограничение для протоколов, чтобы их могли реализовывать только классы. * **`nil`**: В Swift `nil` — это отсутствие значения у опционального типа. Это не указатель на пустую область памяти (как в Objective-C), а специальное значение структуры `Optional.none`.
Дженерики позволяют писать гибкий, переиспользуемый код, избегая дублирования. Они работают со спецификацией типов во время компиляции. * **Принцип работы**: Вместо указания конкретного типа (например, `Int`), используется плейсхолдер (например, `<T>`). Компилятор Swift генерирует оптимизированный специализированный код под конкретные типы при компиляции (специализация дженериков). * **Ограничения типов (Type Constraints)**: Можно ограничить типы, которые могут быть переданы в дженерик, потребовав от них соответствия определенным протоколам (например, `<T: Equatable>`).
* **Opaque Types (`some Protocol`)**: Компилятор знает конкретный тип возвращаемого значения, но скрывает его от вызывающей стороны. Тип фиксирован во время компиляции и не может меняться при повторных вызовах. Пример: `some View` в SwiftUI. * **Existential Types (`any Protocol`)**: Контейнер для любого типа, соответствующего протоколу. Конкретный тип может динамически меняться во время выполнения приложения. Требует упаковки/распаковки значения компилятором, что несет небольшие накладные расходы по производительности.
* **Frame-based Layout**: Координаты (`x, y`) и размеры (`width, height`) элементов задаются вручную через структуру `CGRect`. Требует ручного пересчета позиций при изменении размера экрана или ориентации устройства в методе `layoutSubviews()`. * **Auto Layout**: Позиционирование элементов задается через систему ограничений (Constraints) и правил (отношений между элементами). Система сама вычисляет размеры и координаты динамически, адаптируясь под разные экраны.
Основные этапы жизненного цикла в порядке вызова: 1. `init(coder:)` / `init(nibName:bundle:)`: Инициализация контроллера. 2. `loadView()`: Создание и инициализация корневого view (обычно не переопределяется, если используется Storyboard). 3. `viewDidLoad()`: View загружен в память. Идеальное место для первичной настройки UI и запроса данных. 4. `viewWillAppear(_:)`: View скоро появится на экране. 5. `viewWillLayoutSubviews()` / `viewDidLayoutSubviews()`: Расчет размеров и позиций дочерних view (применение Auto Layout). 6. `viewDidAppear(_:)`: View отобразился на экране. 7. `viewWillDisappear(_:)` / `viewDidDisappear(_:)`: View скрывается или удален с экрана.
В отличие от UIKit, в SwiftUI `View` — это легковесная структура, описывающая интерфейс, а не физический объект на экране. * **Инициализация**: Структура пересоздается каждый раз, когда меняется связанный с ней `@State` или `@Binding`. * **События жизненного цикла**: * `onAppear(perform:)`: Вызывается, когда представление становится видимым на экране. * `onDisappear(perform:)`: Вызывается, когда представление скрывается с экрана. * `task(priority:id:_:)`: Появился в iOS 15. Запускает асинхронную задачу при появлении представления и автоматически отменяет ее, когда представление исчезает.
* **`@State`**: Локальное состояние view. Управляется SwiftUI, хранится вне жизненного цикла самой структуры. * **`@Binding`**: Двусторонняя ссылка на состояние, хранящееся в другом view. Позволяет дочернему view изменять состояние родительского. * **`@ObservedObject`**: Ссылка на внешний класс, реализующий `ObservableObject`. Принуждает view перерисовываться при изменении `@Published` свойств. Жизненным циклом объекта управляет сам разработчик (может пересоздаться при обновлении родительского view). * **`@StateObject`**: Аналогичен ObservedObject, но жизненным циклом объекта управляет SwiftUI. Объект создается ровно один раз и гарантированно не пересоздается при обновлении view. * **`@EnvironmentObject`**: Данные, внедряемые в иерархию view через родительский контейнер. Доступны всем дочерним view без явной передачи.
Combine — это декларативный реактивный фреймворк от Apple для обработки событий во времени. * **Publisher**: Источник данных, который генерирует и отправляет последовательность значений во времени (например, `PassthroughSubject`). * **Subscriber**: Потребитель данных, который подписывается на Publisher для получения значений (например, `sink` или `assign`). * **Operator**: Методы преобразования данных между Publisher и Subscriber (например, `map`, `filter`, `debounce`, `combineLatest`).
`Result` — это перечисление (`enum`) с двумя кейсами: `.success(Value)` и `.failure(Error)`. Он используется для безопасной передачи результата синхронной или асинхронной операции, явно типизируя как успешное значение, так и тип ошибки, без необходимости использовать `try-catch` или опционалы.
* **`DispatchQueue.main.async`**: Добавляет задачу в очередь главного потока и немедленно возвращает управление. Задача будет выполнена асинхронно, когда главный поток освободится. * **`DispatchQueue.main.sync`**: Добавляет задачу в очередь главного потока и блокирует текущий поток до тех пор, пока задача не выполнится. **Внимание**: Вызов этого метода из главного потока приведет к взаимной блокировке (deadlock), так как главный поток будет ждать завершения задачи, которую он сам же заблокировал.
* **`UserDefaults`**: Простой инструмент для хранения пар «ключ-значение». Хранит небольшие объемы данных (настройки, флаги) в формате plist-файла. Не подходит для больших структурированных данных. * **`Core Data`**: Мощный объектно-ориентированный фреймворк для управления графом объектов с персистентным хранилищем (обычно SQLite). Поддерживает сложные связи, миграции схем и кэширование. * **`SwiftData`**: Современный декларативный фреймворк (iOS 17+), заменяющий Core Data. Интегрируется со SwiftUI с помощью макросов (например, `@Model`), делая объявление моделей и работу с базой данных нативной для Swift.
Property Wrapper — это механизм, позволяющий отделить логику управления доступом к свойству в отдельный переиспользуемый класс или структуру. Объявляется с помощью ключевого слова `@propertyWrapper`. Примеры встроенных: `@State`, `@Published`, `@AppStorage`. Позволяет автоматизировать такие задачи, как валидация, логирование или сохранение в кэш.
Блок `defer` содержит код, который будет гарантированно выполнен непосредственно перед выходом из текущей области видимости (scope), независимо от того, как именно происходит выход (обычный return, выброс ошибки или завершение цикла). Идеально подходит для очистки ресурсов, закрытия файлов или деактивации индикаторов загрузки.
Ассоциированный тип (`associatedtype`) — это плейсхолдер для типа, который используется в качестве части определения протокола. Конкретный тип не указывается до тех пор, пока протокол не будет реализован конкретным классом или структурой. Это позволяет создавать универсальные протоколы, работающие с любыми типами.
* **`self`** (с маленькой буквы): Указывает на конкретный текущий экземпляр класса или структуры внутри методов. * **`Self`** (с большой буквы): Используется в протоколах или описаниях методов для обозначения самого типа (класса, структуры), который реализует данный протокол или возвращает данный тип.
Диспетчеризация методов — это механизм, определяющий, как именно компилятор будет вызывать методы во время выполнения. Существует три основных типа: 1. **Direct Dispatch (статическая)**: Самая быстрая. Вызов метода жестко закодирован по адресу. Применяется для методов структур, финальных классов (`final`) и методов в расширениях (`extension`) протоколов. 2. **Table Dispatch (динамическая)**: Использует таблицу виртуальных методов (`vtable`) или свидетельские таблицы протоколов (`witness table`). Используется для переопределяемых методов классов. 3. **Message Dispatch (динамическая в стиле Objective-C)**: Самая гибкая, но медленная. Вызовы происходят через рантайм `objc_msgSend`. Позволяет делать метод-свизлинг (подмену реализации).
`@autoclosure` — это атрибут, который автоматически оборачивает аргумент, переданный в функцию, в замыкание (closure). Это позволяет отложить вычисление переданного аргумента до того момента, пока он не будет вызван внутри функции (ленивое вычисление).
* **`URLSessionDataTask`**: Загружает данные из сети напрямую в оперативную память приложения. Подходит для небольших запросов (REST API, JSON). * **`URLSessionDownloadTask`**: Загружает данные из сети напрямую в файл во временную директорию на диске. Подходит для больших файлов (видео, картинки), экономит оперативную память и может продолжать загрузку в фоновом режиме.
* **Delegate (Делегирование)**: Паттерн связи «один к одному». Конкретный объект-отправитель передает задачи или события ровно одному делегату. Жесткая типизация, высокая производительность. * **NotificationCenter**: Паттерн связи «один ко многим» (Observer). Позволяет отправлять широковещательные сообщения. Отправитель ничего не знает о получателях. Слабая связанность, сложнее отлаживать, требует ручной отписки для предотвращения утечек памяти на старых iOS.
MVVM (Model-View-ViewModel) разделяет обязанности на три слоя: * **Model**: Данные и бизнес-логика. * **View**: SwiftUI View, описывающий интерфейс. Не содержит логики состояния. * **ViewModel**: Класс, реализующий `ObservableObject` (или использующий макрос `@Observable` в iOS 17+). Он преобразует данные из Model в состояние, понятное для View, и обрабатывает действия пользователя.
* **`Array`**: Упорядоченная коллекция элементов. Допуск по индексу — `O(1)`. Поиск элемента — `O(N)`. Допускает дубликаты. * **`Set`**: Неупорядоченная коллекция уникальных элементов. Поиск, вставка и удаление выполняются за `O(1)`, так как элементы хэшируются. Требует соответствия элементов протоколу `Hashable`. * **`Dictionary`**: Неупорядоченная коллекция пар «ключ-значение». Ключи уникальны и должны быть `Hashable`. Доступ по ключу — `O(1)` в среднем.
Чтобы реализовать Copy-on-Write для кастомной структуры, нужно завернуть ее изменяемые свойства в приватный класс (ссылочный тип) и использовать функцию `isKnownUniquelyReferenced`: ```swift class Ref<T> { var val: T init(_ val: T) { self.val = val } } struct MyCoWStruct { private var ref: Ref<[Int]> var data: [Int] { get { ref.val } set { if !isKnownUniquelyReferenced(&ref) { ref = Ref(newValue) // Копируем данные, если ссылка не уникальна } else { ref.val = newValue } } } } ```
* **`Task {}`** (структурированная задача): Наследует контекст выполнения (actor context, приоритеты, локальные переменные задачи) от места своего создания. Если вызывается из `@MainActor`, код внутри `Task {}` по умолчанию будет выполняться на главном потоке. * **`Task.detached {}`** (отделенная задача): Не наследует родительский контекст. Она запускается в независимом глобальном пуле потоков с собственным приоритетом и не привязывается к актору родителя.
По умолчанию все свойства и методы актора изолированы, доступ к ним извне требует асинхронности (`await`). * **`nonisolated`**: Ключевое слово, позволяющее сделать метод или свойство актора неизолированным. К ним можно обращаться синхронно без `await`. * **Ограничение**: Неизолированный метод не может читать или изменять изолированное изменяемое состояние актора. Он может работать только с константами (`let`), неизменяемыми (`Sendable`) типами или вызывать другие неизолированные методы.
Actor Reentrancy (повторное вхождение) — особенность работы акторов. Когда выполнение метода актора приостанавливается на точке ожидания `await` (например, при сетевом запросе), актор освобождает свой поток. В это время актор может принять и начать выполнять другой запрос из своей очереди сообщений. Когда первый асинхронный вызов завершается и управление возвращается к первой функции, состояние актора может измениться. Разработчик должен перепроверять состояние актора после точек `await` во избежание логических багов.
* **Value Types** (структуры, перечисления): Копируются при передаче. Если структура не содержит ссылочных типов внутри себя, она потокобезопасна, так как каждый поток работает со своей изолированной копией в стеке. * **Reference Types** (классы): Несколько потоков могут одновременно хранить ссылки на один и тот же объект в куче. Если объект изменяемый, параллельный доступ приводит к Race Condition и Data Race. Для защиты требуется синхронизация (акторы, замки, очереди).
Оба свойства используются для подписки на изменения наблюдаемых объектов (`ObservableObject`), но они по-разному управляют их жизненным циклом: * **`@StateObject`**: Создает и владеет объектом. SwiftUI инициализирует объект ровно один раз и сохраняет его состояние при перерисовке родительской View. * **`@ObservedObject`**: Не владеет объектом. Объект создается извне. При перерисовке родительской View экземпляр ObservedObject может быть пересоздан заново, что сотрет текущее состояние.
`@Binding` создает двухстороннюю связь (read-write) между View, которая отображает и изменяет состояние, и источником истины (обычно `@State` или `@Published` свойством в родительской View). Она не хранит само значение, а хранит ссылку на геттер и сеттер оригинального свойства, что позволяет дочерней View изменять родительское состояние без дублирования данных.
Основные этапы жизненного цикла контроллера: 1. `init(coder:)` / `init(nibName:bundle:)`: Инициализация. 2. `loadView()`: Создание иерархии View (если создается программно). 3. `viewDidLoad()`: Вью загружена в память. Настройка UI, запросы данных. 4. `viewWillAppear(_:)`: Экран готов появиться на дисплее. 5. `viewWillLayoutSubviews()` / `viewDidLayoutSubviews()`: Расчет размеров и разметка фреймов. 6. `viewDidAppear(_:)`: Экран полностью отобразился. 7. `viewWillDisappear(_:)` -> `viewDidDisappear(_:)`: Экран скрывается с экрана. 8. `deinit`: Выгрузка контроллера из памяти.
* **`frame`**: Описывает положение (x, y) и размер (width, height) UIView относительно системы координат его родительского элемента (Superview). * **`bounds`**: Описывает положение и размер UIView относительно его собственной системы координат. Координаты x и y по умолчанию равны (0, 0). Изменение bounds.origin сдвигает отображаемое содержимое внутри UIView (так работают скролл-вью).
Приоритеты констрейнтов (Priority) определяют важность ограничений интерфейса. Приоритеты выражаются числом от 1 до 1000: * **1000 (Required)**: Обязательное ограничение. Если система не может его удовлетворить, в консоль сыпятся ошибки разметки. * **250–999 (Optional)**: Опциональные приоритеты (High, Low). Используются для динамической адаптации интерфейса. Если возникает конфликт ограничений, Auto Layout ломает констрейнт с меньшим приоритетом.
Responder Chain (цепочка респондеров) — это иерархический путь объектов класса `UIResponder` (UIView, UIViewController, UIWindow, UIApplication), предназначенный для обработки и передачи сенсорных событий (touches, gestures, press events). Если первая View (First Responder) не может обработать событие, оно передается вверх по цепочке к superview, затем к контроллеру, далее к UIWindow и UIApplication, пока не будет обработано или проигнорировано.
Стек Core Data состоит из четырех ключевых компонентов: 1. **NSManagedObjectModel**: Описывает схему базы данных (сущности, их атрибуты и связи). 2. **NSManagedObjectContext**: Песочница для работы с данными. Здесь создаются, редактируются и удаляются объекты в оперативной памяти. 3. **NSPersistentStoreCoordinator**: Посредник между контекстом и физическим хранилищем. Отвечает за сериализацию объектов. 4. **NSPersistentStore**: Непосредственный физический файл базы данных (обычно SQLite) на диске устройства.
`NSManagedObjectContext` привязан к конкретному потоку. Обращение к объектам из другого потока вызывает крэш. Для многопоточности используют два основных подхода: * **Private Queue Context**: Создание контекста с типом `.privateQueueConcurrencyType`. Все операции с ним оборачиваются в блоки `.perform {}` или `.performAndWait {}`. * **Parent/Child Contexts**: Фоновый (child) контекст делает сложные вычисления и сохраняет изменения в родительский (parent, Main Queue) контекст, который отвечает за отображение в UI.
* **`some Protocol`** (Opaque Type): Компилятор знает конкретный тип объекта, возвращаемого функцией, но скрывает его от вызывающего кода. Возвращаемый тип фиксирован во время компиляции. Используется для SwiftUI View. * **`any Protocol`** (Existential Type): Контейнер для любого типа, реализующего протокол. Конкретный тип может динамически меняться во время выполнения (рантайма). Требует упаковки/распаковки данных в памяти, что медленнее.
`Result<Value, Error>` — это перечисление (Enum) с двумя кейсами: `.success(Value)` и `.failure(Error)`. Он используется для явной передачи результатов асинхронных операций (например, сетевых запросов), когда возвращаемое значение и ошибка не могут сосуществовать одновременно, в отличие от старого подхода с кортежами `(Data?, Error?)`.
Property Wrapper — это структура или класс, помеченные атрибутом `@propertyWrapper`. Они инкапсулируют логику доступа к свойству: ```swift @propertyWrapper struct Trimmed { private var value: String = "" var wrappedValue: String { get { value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(wrappedValue: String) { self.wrappedValue = wrappedValue } } ```
* **`@dynamicMemberLookup`**: Позволяет обращаться к несуществующим свойствам типа во время компиляции через синтаксис точек. Вызов транслируется в метод `subscript(dynamicMember:)`. * **`@dynamicCallable`**: Позволяет вызывать экземпляры объектов как функции (подобно замыканиям). Вызов транслируется в метод `dynamicallyCall(withArguments:)` или `dynamicallyCall(withKeywordArguments:)`.
* **AppDelegate**: Отвечает за жизненный цикл самого приложения (запуск, закрытие, получение push-уведомлений, фоновые задачи). Начиная с iOS 13, не управляет окнами приложения. * **SceneDelegate**: Управляет жизненным циклом конкретных оконных сцен (Scenes). Отвечает за активное состояние интерфейса (`sceneDidBecomeActive`), переходы в фоновый режим для конкретного окна и поддержку мультиоконности на iPadOS.
Swift использует три основных типа диспетчеризации методов: 1. **Direct/Static Dispatch**: Прямой вызов адреса функции (структуры, final классы, extension). Самый быстрый. 2. **Table/Virtual Dispatch**: Поиск адреса метода в таблице (vtable/witness table) класса во время выполнения. Поддерживает наследование. 3. **Message Dispatch**: Диспетчеризация Objective-C рантайма через `objc_msgSend`. Самая медленная, но позволяет подменять методы на лету (Method Swizzling).
Whole Module Optimization — режим оптимизации компилятора Swift. По умолчанию файлы компилируются по отдельности, что мешает оптимизациям на стыке файлов. WMO компилирует весь модуль целиком, позволяя компилятору инлайнить функции, оптимизировать ARC-вызовы и удалять неиспользуемые генерики.