Добавить в корзинуПозвонить
Найти в Дзене

Swift Interview. Вопросы с реального собеседования на позицию миддла

Всем привет! Сегодня у нас новый формат: вопросы будут вместе с краткими ответами 🔥🔥🔥 Само собеседование больше похоже на некий срез, потому что обошли много тем. Можете проверить себя 😎😎😎 Ссылка на канал в телеграмме. Не забывайтесь подписываться 🚀🚀🚀
1. Что такое ARC, для чего нужно, когда работает? ARC (Automatic Reference Counting) — это технология управления памятью, используемая в Swift и Objective-C. Она работает только с reference types (классы, замыкания). Как работает: Когда работает: ARC действует постоянно во время выполнения программы. Каждый доступ к ссылочному объекту (присваивание, передача в функцию, возврат, добавление в массив) может изменить счетчик. Зачем нужен: Чтобы программист не писал вручную retain/release (как в MRC в Objective-C). ARC автоматизирует это на этапе компиляции, не требуя сборщика мусора. Важно: ARC не предотвращает и не разрушает циклические ссылки. Это задача программиста — использовать weak/unowned. Время работы
ARC На этапе компиля
Оглавление

Всем привет! Сегодня у нас новый формат: вопросы будут вместе с краткими ответами 🔥🔥🔥 Само собеседование больше похоже на некий срез, потому что обошли много тем. Можете проверить себя 😎😎😎 Ссылка на канал в телеграмме. Не забывайтесь подписываться 🚀🚀🚀

1. Что такое ARC, для чего нужно, когда работает?

ARC (Automatic Reference Counting) — это технология управления памятью, используемая в Swift и Objective-C. Она работает только с reference types (классы, замыкания).

Как работает:

  • Каждый объект класса имеет счетчик сильных ссылок (retain count), хранящийся в его заголовке (обычно 64 бита, часть — для других флагов).
  • При создании объекта счетчик = 1.
  • При присваивании новой сильной ссылки (let x = object) компилятор вставляет вызов swift_retain(object) — счетчик увеличивается.
  • При выходе ссылки из области видимости или переприсваивании вставляется swift_release(object) — счетчик уменьшается.
  • Когда счетчик становится 0, объект немедленно уничтожается (вызывается deinit, память освобождается).

Когда работает: ARC действует постоянно во время выполнения программы. Каждый доступ к ссылочному объекту (присваивание, передача в функцию, возврат, добавление в массив) может изменить счетчик.

Зачем нужен: Чтобы программист не писал вручную retain/release (как в MRC в Objective-C). ARC автоматизирует это на этапе компиляции, не требуя сборщика мусора.

Важно: ARC не предотвращает и не разрушает циклические ссылки. Это задача программиста — использовать weak/unowned.

2. Чем ARC отличается от Garbage Collector?

Сравнение:

Время работы
ARC На этапе компиляции вставляются вызовы retain/release
GC Во время выполнения работает отдельный поток
Детерминизм
ARC Объект удаляется сразу, когда счетчик = 0
GC Удаление отложено до следующего цикла GC
Паузы
ARC Нет пауз
GC Возможны "stop-the-world" паузы (особенно в старых GC)
Циклические ссылки
ARC Приводят к утечкам, нужно weak/unowned
GC Обрабатываются автоматически (GC видит недостижимость)
Накладные расходы
ARC На каждый retain/release — небольшие, но частые
GC Периодически большие затраты на маркировку и сжатие
Память
ARC Освобождается сразу, пики использования ниже
GC Может держать память дольше, но лучше общая пропускная способность

Итог: ARC лучше для систем, где важна предсказуемость задержек (UI, игры, аудио). GC лучше для серверов с большим количеством краткоживущих объектов.

3. Какие есть возможности для исправления Retain Cycle?

Retain cycle (цикл сильных ссылок) — когда объекты A и B сильно ссылаются друг на друга (или цепочка ссылок возвращается к началу). Ни один не может быть удален, даже если они не нужны.

Способы исправления:

  1. weak ссылка — превращает одну из ссылок в опциональную, которая автоматически становится nil при удалении объекта
    Parent {
    var child: Child?
    }
    class Child {
    weak var parent: Parent? // weak разрывает цикл
    }
  2. unowned ссылка — как weak, но не опционал. Используется, когда объект точно будет существовать всё время жизни ссылки
    Child {
    unowned let parent: Parent
    init(parent: Parent) { self.parent = parent }
    }

  3. Capture list в замыканиях
    ViewController {
    var closure: (() -> Void)?
    func setup() {
    closure = { [weak self] in
    self?.doSomething()
    }
    }
    }

  4. Ручной разрыв цикла (редко, но бывает)
    cleanup() {
    self.closure = nil
    }
  5. Переход на value types (структуры, перечисления) — они не создают циклов, так как не используют счетчик ссылок.
  6. Использование [unowned self] только когда self точно не может быть nil (например, в анимациях, где замыкание выполняется синхронно).

Диагностика: Xcode Memory Graph Debugger, Instruments (Leaks), статический анализатор.

4. В чем отличие weak и unowned?

Сравнение:

Тип
weak Всегда опционал (?)
unowned Неопционал
Значение при удалении объекта
weak Становится nil
unowned Остается указателем на удаленную память (висячая ссылка)
Обращение после удаления
weak nil — можно проверить
unowned Crash (EXC_BAD_ACCESS)
Безопасность
weak Высокая
unowned Низкая (требует уверенности)
Производительность
weak Чуть медленнее (нужен рантайм для обнуления)
unowned Быстрее (прямой доступ)
Использование
weak Делегаты, произвольные связи
unowned parent в дочерних объектах, когда parent живет дольше
Обязательная проверка
weak Да (if let, guard let, ?)
unowned Нет (но нужно быть уверенным)

Правило: По умолчанию используйте weak. unowned — только когда вы на 100% уверены, что объект не может быть удален раньше ссылки. Ошибка = crash.

5. Расскажи подробнее про sideTable.

SideTable — это структура из Swift runtime (определена в swift/Runtime/HeapObject.h). Она подключается к объекту лениво, когда встроенного в заголовок объекта места недостаточно.

Стандартный заголовок объекта (HeapObject) содержит:

  • указатель на класс (isa)
  • inline refcount (обычно 64 бита: часть — strong count, часть — weak count, часть — флаги)

Но 64 бита не могут вместить произвольное количество слабых ссылок или associated objects. Тогда создается SideTable.

Что хранится в SideTable:

struct SideTable {
SpinLock lock; // для потокобезопасности
RefCounts weakRefCount; // счетчик слабых ссылок
WeakEntry *weakEntries; // список слабых ссылок
ObjectAssociationMap *associations; // associated objects
bool isDeiniting; // флаг деинициализации
}

Когда создается SideTable:

  • Первая weak ссылка на объект
  • Первый вызов objc_setAssociatedObject
  • При переполнении inline refcount

Как работает удаление объекта:

  1. Сильные ссылки обнуляются, счетчик становится 0.
  2. Если есть SideTable — runtime проходит по weakEntries и обнуляет каждую слабую ссылку.
  3. Удаляется сам объект, затем SideTable (если она была).

Почему это важно: SideTable позволяет эффективно поддерживать слабые ссылки и associated objects без выделения памяти для каждого объекта. Плата — дополнительная косвенность и блокировка (SpinLock) при доступе к SideTable.

6. Расскажи про capture list, про захват сильных ссылок, замыкания.

Замыкание (closure) в Swift — это reference type. Когда замыкание использует переменные из окружающего контекста, оно их захватывает.

По умолчанию (без capture list):

  • Reference types (классы, замыкания) — захватываются сильно (retain).
  • Value types (структуры, Int, String) — копируются (но если внутри value type есть ссылка, она копируется сильно).

Capture list — синтаксис:

{ [weak self, unowned delegate, x = someValue, y] in
// тело
}

Варианты захвата:

  • [self] — сильный захват self (Swift 5.3+). Используется явно.
  • [weak self] — слабый опциональный захват.
  • [unowned self] — безусловный захват (опасно).
  • [x = someValue] — захват значения someValue под именем x (копирование или strong).
  • [x] — краткая форма для захвата x (если x — value type, копия; если reference — strong).

Пример проблемы (retain cycle):


class ViewController {
var onTap: (() -> Void)?
func setup() {
onTap = {
self.doSomething() // сильный захват self
}
}
}

Здесь self → onTap → self (цикл).

Исправление:

onTap = { [weak self] in
self?.doSomething()
}

Нюансы:

  • [weak self] делает self опционалом внутри замыкания. Нужно писать self?. или guard let self.
  • [unowned self] — не опционал, но при обращении к удаленному self — crash.
  • Если внутри замыкания несколько сильных захватов, а один weak — остальные остаются сильными, если явно не указано иное.
  • Замыкания, захваченные слабо, не продлевают жизнь объекту.

7. Что будет, если все замыкание сделать escaping? Убрать все escaping? Нужен ли для опционального замыкания escaping?

@escaping — что это?

По умолчанию замыкания в Swift неэскейпинговые (non-escaping). Это значит, что замыкание выполняется до возврата из функции и не переживает её. Компилятор может оптимизировать его хранение на стеке.

@escaping говорит: "Это замыкание может быть сохранено и выполнено после того, как функция вернётся".

Что будет, если все замыкания сделать escaping?

  • Плюсы: Код станет более гибким (можно хранить, передавать в другие потоки).
  • Минусы:
    Компилятор не может оптимизировать стековое выделение.
    Нужно явно указывать self в capture list (non-escaping может автоматически подразумевать self).
    Увеличивается расход памяти (замыкания попадают в кучу).
    Retain cycle становится более вероятным.
    Производительность чуть ниже.

Что будет, если убрать все escaping?

  • Большинство асинхронных API перестанут работать:swiftDispatchQueue.main.async { } // ошибка: замыкание escaping
  • Нельзя будет сохранить замыкание в свойство.
  • Нельзя будет передать замыкание в другой поток.
  • Код станет более предсказуемым, но намного менее функциональным.

Нужен ли escaping для опционального замыкания?

var closure: (() -> Void)? // не escaping по умолчанию, но при хранении — нужно @escaping
func take(closure: (() -> Void)?) { } // не escaping
func takeEscaping(closure: @escaping (() -> Void)?) { } // escaping

Ответ: Опциональность не влияет на escaping. Если вы сохраняете замыкание (например, присваиваете свойству), оно должно быть @escaping. Если просто вызываете внутри функции — не нужно.

8. Что такое associated type?

Associated type — это дженерик-заполнитель внутри протокола. Он позволяет протоколу абстрагироваться от конкретного типа, оставляя решение реализации.

Пример:

protocol Stack {
associatedtype Element
mutating func push(_ element: Element)
mutating func pop() -> Element?
}

Реализации:

struct IntStack: Stack {
typealias Element = Int // можно явно, но Swift выведет сам
private var items: [Int] = []
mutating func push(_ element: Int) { items.append(element) }
mutating func pop() -> Int? { items.popLast() }
}


struct GenericStack<Element>: Stack {
private var items: [Element] = []
mutating func push(_ element: Element) { items.append(element) }
mutating func pop() -> Element? { items.popLast() }
}

Ограничения:

  • Протокол с associated type нельзя использовать как обычный тип: var stack: Stack — ошибка. Нужен type erasure (AnyStack<Element>) или some Stack.
  • Associated types могут иметь требования (например, associatedtype Element: Hashable).

Связь с дженериками:

Дженерик-класс: тип указывается при объявлении переменной.
Associated type: тип определяется реализацией протокола.

9. Что такое Hashable, для чего нужен? Что такое коллизия?

Hashable — протокол, требующий от типа способности вычислять хеш-значение. Наследуется от Equatable (нужно уметь сравнивать на равенство).

Требования:

protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}

Где нужен Hashable:

  • Ключи в Dictionary (ключ должен быть хешируемым)
  • Элементы в Set
  • Использование в качестве enum с raw value (тоже требует Hashable)
  • Ускорение поиска, сравнения

Как работает:

  1. hash(into:) смешивает свойства объекта в Hasher.
  2. Hasher использует алгоритм (SipHash-1-3, затем — более новый) для получения 64-битного хеша.
  3. Dictionary использует хеш для определения корзины (bucket index).

Коллизия — что это?

Коллизия — ситуация, когда два разных объекта дают одинаковый хеш.

Как Dictionary обрабатывает коллизии:

  • Открытая адресация (Swift Dictionary использует что-то близкое к этому): ищет следующую свободную корзину.
  • Цепочки (Java HashMap): каждая корзина — это связный список.

Последствия коллизий:

  • Поиск/вставка/удаление из O(1) может деградировать до O(n) в худшем случае.
  • Хорошая хеш-функция минимизирует коллизии.

Пример реализации:

struct Person: Hashable {
var name: String
var age: Int

func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(age)
}

static func == (lhs: Person, rhs: Person) -> Bool {
lhs.name == rhs.name && lhs.age == rhs.age
}
}

10. Какие объекты могут быть ключом и значением в Dictionary?

Ключ (Key):

  • Должен реализовывать протокол Hashable.
  • Все стандартные типы Swift, кроме функций и некоторых нестабильных типов: Int, String, Double, Bool, URL, IndexPath, Date, Character, UUID, Data, Range, Array (если элементы Hashable), Set (если элементы Hashable), Dictionary (если ключи и значения Hashable), Optional (если Wrapped Hashable).
  • Кастомные структуры и классы, если реализуют Hashable.

Значение (Value):

  • Любой тип. Нет ограничений.
  • Может быть классом, структурой, перечислением, замыканием, протоколом, Any, Optionalи т.д.

Важные нюансы:

  • Если ключ — класс, то два разных объекта с одинаковыми полями считаются разными ключами (по умолчанию сравнение по указателю). Нужно переопределить == и hash(into:).
  • После добавления объекта в словарь нельзя изменять свойства, участвующие в хеше — иначе словарь не сможет его найти. Лучше использовать value types для ключей.

Пример с кастомным ключом:

struct UserID: Hashable {
let value: Int
}
var dict = [UserID: String]()
dict[UserID(value: 1)] = "Alice"

11. Расскажи про Value и Reference type.

Value Types (типы-значения):

  • Примеры: struct, enum, tuple, Int, String, Array, Dictionary, Set, Optional
  • Хранение: обычно на стеке (если не внутри reference type или не захвачены замыканием). Большие массивы/словари хранят данные в куче, но сам контейнер — value.
  • При присваивании: создается полная копия (глубокая или поверхностная — зависит от содержимого, но формально — новый экземпляр).
  • ARC: не участвуют (нет счетчика ссылок на сам value type, но если внутри есть ссылки, они управляются ARC).
  • Изменяемость: для изменения нужен var. Константа (let) означает неизменяемость всего графа (но если внутри есть ссылки, они могут меняться, так как ссылка сама не меняется).
  • Потокобезопасность: по умолчанию безопаснее (каждый поток работает со своей копией).
  • Наследование: не поддерживают.

Reference Types (типы-ссылки):

  • Примеры: class, closure
  • Хранение: в куче (heap). Стек хранит указатель.
  • При присваивании: копируется ссылка (указатель). Объект один.
  • ARC: управляются автоматическим подсчетом ссылок.
  • Изменяемость: можно менять свойства даже у let (сама ссылка константна, но объект может меняться).
  • Потокобезопасность: нужно синхронизировать доступ (data races).
  • Наследование: поддерживают (одиночное).

Выбор:

  • Value types для данных, которые не должны разделяться (координаты, настройки, модели-фрагменты).
  • Reference types для общих ресурсов, делегатов, идентичности (один объект должен быть виден везде).

Пример:

struct S { var x = 0 }
class C { var x = 0 }

var a = S(); var b = a
a.x = 5 // b.x == 0 (копия)

var c = C(); var d = c
c.x = 5 // d.x == 5 (тот же объект)

12. Расскажи подробнее про коллекции. Может ли в словаре поиск элемента быть O(n)?

Коллекции в Swift:

Array (ContiguousArray, ArraySlice)

  • Хранение: непрерывный блок памяти (copy-on-write).
  • Доступ по индексу: O(1)
  • Вставка/удаление в конце: амортизированно O(1) (иногда реаллокация)
  • Вставка/удаление в середине: O(n) (сдвиг элементов)
  • Поиск значения (firstIndex): O(n)
  • Поиск по индексу: O(1)

Dictionary

  • Хранение: хеш-таблица с открытой адресацией (или гибридная). Swift использует NativeDictionary с кэшированием хешей.
  • Поиск по ключу: в среднем O(1). В худшем случае O(n).
  • Вставка/удаление: аналогично поиску.

Set

  • Аналогичен Dictionary, но только ключи.

Может ли поиск в словаре быть O(n)?

ДА, в следующих случаях:

  1. Сильные коллизии хешей.
    Если все ключи дают одинаковый хеш, они попадают в одну корзину.
    Словарь вынужден проверять каждый ключ на равенство (O(n)).
    Пример: плохая хеш-функция, или злонамеренные данные.
  2. При рехешировании (rehashing).
    Когда словарь заполняется выше порога (обычно ~75%), он создает новый массив большего размера и перехеширует все элементы.
    Во время рехешинга операции могут быть медленнее, но это не O(n) для каждой операции, а амортизированная стоимость.
  3. Если ключ — класс с неправильным hash.
    Например, hash всегда возвращает 0.

Почему в среднем O(1)?

  • Хорошая хеш-функция распределяет ключи равномерно.
  • Размер корзин растет пропорционально количеству элементов.
  • Вероятность коллизий низкая.

Пример деградации:


struct BadHash: Hashable {
let value: Int
func hash(into hasher: inout Hasher) {
hasher.combine(0) // всегда 0
}
}
var dict = [BadHash: Int]()
for i in 0..<1000 {
dict[BadHash(value: i)] = i // все в одной корзине, поиск O(n)
}

13. Как работает sort в Swift, за какую сложность?

Алгоритм: TimSort

TimSort — гибридный стабильный алгоритм сортировки, изобретенный Тимом Петерсом для Python. Swift использует его модификацию.

Принцип работы:

  1. Разбиение на "run" (естественные или искусственные последовательности).
    Ищет уже отсортированные подмассивы (возрастающие или убывающие).
    Если run слишком короткий (< 32 элементов), добивает до минимальной длины сортировкой вставками.
  2. Слияние (merge) run в стеке.
    Поддерживает инварианты размеров (как в merge sort).
    Использует временный буфер для слияния.

Сложность:

  • Худший случай: O(n log n)
  • Лучший случай (уже отсортирован): O(n)
  • Средний случай: O(n log n)
  • Память: O(n) в худшем случае (для временного буфера)

Стабильность:

  • Стабильная — элементы с равными ключами сохраняют порядок.

Почему не quick sort?

  • Quick sort нестабилен, худший случай O(n²) (если неудачный выбор опорного элемента).
  • TimSort быстрее на реальных данных (частично отсортированных).

Как использовать:

var numbers = [3, 1, 4, 1, 5]
numbers.sort() // по возрастанию, мутирующий
let sorted = numbers.sorted() // возвращает новый массив
numbers.sort(by: >) // по убыванию

Для замыкания сравнения:


numbers.sort { $0.value < $1.value }

14. Что такое defer?

defer — это механизм отложенного выполнения кода. Блок, помеченный defer, выполняется при выходе из текущей области видимости, независимо от того, как произошел выход (через return, throw, break, или просто достижение конца).

Синтаксис:

defer {
// код, который выполнится при выходе
}

Важные правила:

  1. Несколько defer выполняются в обратном порядке объявления (стек).

    defer { print("1") }
    defer { print("2") }
    // Вывод: 2, затем 1

  2. Область видимости — блок (функция, цикл, do-блок, if).
  3. Переменные видны — defer видит все переменные на момент выхода, даже если они были изменены после объявления defer.
  4. Нельзя break/return внутри defer (но можно в самом defer, это не запрещено, но странно).
  5. Defer выполняется даже при ошибке (при throw).

Пример использования:

func processFile(filename: String) throws {
let file = openFile(filename)
defer {
closeFile(file) // закроем в любом случае
}
// ... работа с файлом, возможен throw
// closeFile вызовется здесь
}

Где полезен:

  • Освобождение ресурсов (файлы, блокировки, сетевые соединения)
  • Логирование времени выхода из функции
  • Сброс временного состояния
  • performBatchUpdates в UIKit

Нюанс с return:

func test() -> Int {
defer { print("defer") }
return 10
}
// Сначала вычисляется return (10), затем defer, затем возврат.

15. Расскажи про области видимости private, open и т.д.

Swift имеет 5 уровней доступа (от самого открытого к самому закрытому):

1. open

  • Доступен везде (внутри модуля и вне его)
  • Можно наследовать класс и переопределять методы/свойства за пределами модуля
  • Только для классов и их членов (не для структур, enum)

2. public

  • Доступен везде (внутри и вне модуля)
  • Нельзя наследовать и переопределять вне модуля
  • Можно использовать, но нельзя подменять поведение

3. internal (по умолчанию)

  • Доступен только внутри модуля
  • Вне модуля — недоступен

4. fileprivate

  • Доступен в пределах одного файла
  • Любые типы в этом файле (не обязательно вложенные) имеют доступ

5. private

  • Доступен только в пределах лексической области (внутри того же типа/расширения, если расширение в том же файле)
  • В Swift private распространяется на расширения того же типа в том же файле (в отличие от многих языков)

Особенности:

Расширения и private:

class MyClass {
private var x = 0
}
extension MyClass {
func foo() {
print(x) // Доступно, если extension в том же файле
}
}

Геттеры/сеттеры:

public private(set) var name = "Alice" // публичный геттер, приватный сеттер

open vs public — пример:

// Модуль A
open class ParentOpen {
open func method() { }
public func publicMethod() { }
}
public class ParentPublic {
public func method() { }
}

// Модуль B (импортирует A)
class ChildOpen: ParentOpen {
override func method() { } // можно
// override func publicMethod() { } // нельзя — не open
}
class ChildPublic: ParentPublic {
// override func method() { } // нельзя — класс public, но метод public
}

Доступ для протоколов:

  • Протокол не может быть менее доступным, чем его требования.

16. Что такое typealias?

typealias — объявление псевдонима для существующего типа. Не создает новый тип, а только альтернативное имя.

Синтаксис:

typealias NewName = ExistingType

Примеры использования:

  1. Упрощение сложных типов:

typealias JSON = [String: Any]
typealias CompletionHandler = (Result<Data, Error>) -> Void
typealias Point = (x: Int, y: Int)

2. Дженерики:

typealias StringDictionary<T> = Dictionary<String, T>
typealias IntArray = Array<Int>

3. Замыкания:

typealias VoidClosure = () -> Void

4. Связанные типы в протоколах:

protocol StringRepresentable {
associatedtype StringType
}
extension StringRepresentable where Self: CustomStringConvertible {
typealias StringType = String
}

5. Совместимость с Objective-C:

typealias MyBlock = @convention(block) (Int) -> String

Ограничения:

  • Нельзя создать typealias для subscript или свойства.
  • Можно создавать вложенные:

struct Outer {
typealias Inner = Int
}
let x: Outer.Inner = 5

Чем отличается от associatedtype?

  • typealias — это просто имя для конкретного типа.
  • associatedtype — это дженерик-заполнитель в протоколе, который будет определен реализацией.

17. Можем ли мы что-нибудь хранить в extension?

Да, но с ограничениями.

Что можно добавлять в extension:

1.Вычисляемые свойства (computed properties):

extension Int {
var isEven: Bool { return self % 2 == 0 }
}

2. Методы (instance и type):

extension String {
func reversed() -> String { ... }
static func empty() -> String { return "" }
}

3. Сабскрипты (subscripts):

extension Array {
subscript(safe index: Int) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}

4. Инициализаторы:
Для структур и классов можно добавлять новые init, но:
Для классов — только convenience init, если не использовать required.
Для структур — любой init, но если у структуры есть автоматический memberwise init, то после добавления кастомного init в extension он сохраняется (в отличие от добавления внутри самой структуры).

extension UIView {
convenience init(color: UIColor) {
self.init(frame: .zero)
backgroundColor = color
}
}

5. Вложенные типы:

extension Int {
enum Kind { case negative, zero, positive }
var kind: Kind {
if self < 0 { return .negative }
else if self > 0 { return .positive }
else { return .zero }
}
}

6. Conformance to protocols (основное применение extension):

extension MyType: SomeProtocol { ... }

Чего нельзя добавлять:

1. Хранимые свойства (stored properties):

extension UIView {
var myProperty: String = "error" // ОШИБКА
}

Обход для классов через associated objects (но не чисто Swift):

private var key: Void?
extension UIView {
var myProperty: String {
get { return objc_getAssociatedObject(self, &key) as? String ?? "" }
set { objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}

2. Переопределение существующих методов (можно только если метод из протокола, и то через ограничения).

3. Хранимые свойства класса (static let — можно, это тип, не экземпляр).

4. Наблюдатели свойств (willSet/didSet) для существующих свойств — нельзя.

Почему нельзя хранимые свойства?

  • Extension не может добавлять память к существующему типу. Структура объекта фиксирована в момент компиляции.

18. Что такое static string?

Скорее всего, имеется в виду static let строковое свойство или строковый литерал, хранящийся статически.

static let string:

struct Constants {
static let appName = "MyApp"
static let apiKey = "123456"
}

  • Существует в единственном экземпляре на весь тип.
  • Лениво инициализируется (при первом обращении), но static let — атомарно и потокобезопасно (Swift гарантирует).
  • Не создает копий при каждом использовании.

Статическая строка в C/Objective-C смысле:

В Swift строки — value types (String). При объявлении let s = "Hello" строка может храниться в сегменте данных (как строковой литерал в бинарнике), а при использовании — копироваться (COW). Но Swift не делает различия между "статической" и "динамической" строкой на уровне синтаксиса.

Нюансы:

  • static let в структуре — каждый тип имеет свою копию.
  • static let в классе — аналогично, но классы могут быть переопределены, а статические свойства — нет.
  • static var (не let) — изменяемое статическое свойство, но не потокобезопасно без синхронизации.

Пример использования:`

class User {
static let defaultAvatar = "avatar.png"
static var totalUsers = 0 // mutable static
}

19. Что такое @autoclosure?

@autoclosure — атрибут параметра функции, который автоматически оборачивает переданное выражение в замыкание. Позволяет писать вызывающий код без явных фигурных скобок {}.

Синтаксис:

func assert(_ condition: @autoclosure () -> Bool, _ message: String) {
if !condition() {
print(message)
}
}

Вызов:

assert(2 + 2 == 5, "Math is broken")
// 2 + 2 == 5 автоматически превращается в { 2 + 2 == 5 }

Зачем нужно?

  1. Ленивое вычисление — выражение вычисляется только при вызове замыкания.
  2. Синтаксический сахар — не нужно писать { }.
  3. Условное вычисление — например, в assert, precondition, где выражение может быть дорогим и вычисляется только при ошибке.

Пример с отложенным вычислением:

func orElse(_ a: Bool, _ b: @autoclosure () -> Bool) -> Bool {
if a { return true }
return b() // вычисляется только если a == false
}

orElse(true, expensiveComputation()) // expensiveComputation не вызовется

Ограничения:

  • @autoclosure можно применять только к параметрам замыканий без аргументов (() -> T).
  • По умолчанию @autoclosure — не escaping. Если нужно сохранить замыкание, добавляют @autoclosure @escaping.
  • Не злоупотреблять — ухудшает читаемость, если используется не по назначению.

Пример с escaping:

var closures: [() -> Bool] = []
func add(_ closure: @autoclosure @escaping () -> Bool) {
closures.append(closure)
}

20. Что такое some, any?

Это ключевые слова для работы с протоколами в Swift 5.1+ (some) и Swift 5.6+ (any).

some — Opaque Type (непрозрачный тип)

Говорит: "Функция возвращает один конкретный тип, но я не хочу его называть". Компилятор знает тип, но вызывающий код — нет.

func makeView() -> some UIView {
return UIButton() // всегда UIButton, но наружу виден как some UIView
}

Ограничения:

  • Должен возвращать один и тот же тип во всех ветках (нельзя return UIButton() и return UILabel()).
  • Сохраняет типобезопасность (в отличие от any).

Где полезно:

  • Скрыть сложные дженерики (например, some Collection)
  • Работа с протоколами, имеющими associatedtype или Self требования
  • Возврат some View в SwiftUI

any — Existential Type (экзистенциальный тип)

Говорит: "Я стираю конкретный тип и работаю с ним через протокол динамически".

let views: [any UIView] = [UIButton(), UILabel()]

Особенности:

  • Может хранить разные типы, соответствующие протоколу.
  • Использует динамический dispatch (через witness table).
  • Имеет накладные расходы (упаковка в контейнер).
  • Нельзя использовать с протоколами, имеющими Self или associatedtype (без type erasure).

Сравнение:


Тип
some Protocol Один конкретный тип
any Protocol Любой тип, соответствующий протоколу
Динамичность
some Protocol Статический (известен компилятору)
any Protocol Динамический (unknown)
Накладные расходы
some Protocol Нет
any Protocol Есть (existential container)
Работа с associatedtype
some Protocol Да
any Protocol Нет (нужен type erasure)
Разные ветки return
some Protocol Нельзя
any Protocol Можно
Массив разных типов
some Protocol Нельзя
any Protocol Можно

Когда что использовать:

  • some — для возврата однотипных объектов, сохраняя абстракцию.
  • any — для коллекций с разными типами, для стирания типов.

Пример:

protocol P { associatedtype T }
func f() -> some P { ... } // OK
// func g() -> any P { ... } // ERROR: associatedtype

21. Что такое copy-on-write?

Copy-on-Write (COW) — оптимизация для value types, при которой реальное копирование данных происходит только в момент изменения одной из копий, а не в момент присваивания.

Как работает в Swift:

  1. Присваивание (var a = b):
    Создается новая структура-обертка, но внутренний буфер данных
    разделяется (через ссылку на выделенную в куче память).
    Счетчик ссылок на буфер увеличивается.
  2. Изменение (a.append(5)):
    Проверяется isUniquelyReferenced() (или _isUnique).
    Если на буфер есть только одна ссылка (текущая) — можно менять на месте.
    Если несколько (например, b тоже ссылается) — создается
    новая копия буфера, затем изменение.

Где используется в стандартной библиотеке:

  • Array
  • Dictionary
  • Set
  • String

Пример:

var a = [1, 2, 3]
var b = a // копирования нет, общий буфер
b.append(4) // здесь происходит реальное копирование (COW)

// теперь a = [1,2,3], b = [1,2,3,4]

Как реализовать свой COW:

final class Ref<T> {
var value: T
init(_ value: T) { self.value = value }
}


struct COW<T> {
private var ref: Ref<T>

init(_ value: T) {
ref = Ref(value)
}

var value: T {
get { ref.value }
set {
if !isKnownUniquelyReferenced(&ref) {
ref = Ref(newValue)
return
}
ref.value = newValue
}
}
}

Преимущества:

  • Экономит память и время для больших value types.
  • Позволяет value types эффективно работать с большими данными.

Недостатки:

  • Накладные расходы на проверку уникальности.
  • Нужно осторожно с многопоточностью (проверка не атомарна без дополнительной синхронизации).

22. Что такое type erasure?

Type Erasure — паттерн, который скрывает конкретный тип (особенно с associatedtype или Self требованиями) за общей оберткой, позволяя работать с разными типами через единый интерфейс.

Зачем нужен?

Протокол с associatedtype нельзя использовать как any Protocol:

protocol Container {
associatedtype Item
func get() -> Item
}
// let c: any Container = ... // Ошибка!

Решение — Type Erasure:

Классическая реализация:

struct AnyContainer<Item>: Container {
private let _get: () -> Item

init<C: Container>(_ container: C) where C.Item == Item {
_get = { container.get() }
}

func get() -> Item { return _get() }
}

Примеры в Swift:

  • AnySequence, AnyCollection, AnyIterator
  • AnyPublisher (Combine)
  • AnyCancellable
  • AnyHashable

Как работает AnyHashable:

let set: Set<AnyHashable> = [1, "hello", 3.14]

AnyHashable хранит внутри исходное значение и замыкания для == и hash.

Реализация простого type erasure:

protocol Drawable {
func draw()
}

struct AnyDrawable: Drawable {
private let _draw: () -> Void

init<T: Drawable>(_ drawable: T) {
_draw = { drawable.draw() }
}

func draw() { _draw() }
}

let items: [AnyDrawable] = [AnyDrawable(Circle()), AnyDrawable(Square())]

Альтернативы:

  • some — если нужен один конкретный тип.
  • any — если протокол не имеет associated types.
  • Type erasure — когда нужно хранить коллекцию разных типов с associated types.

Недостатки type erasure:

  • Увеличение кода (ручная обертка).
  • Потеря некоторых типовых ограничений.
  • Накладные расходы на вызовы через замыкания.

23. Какие проблемы с многопоточностью? Какие способы работы с многопоточностью есть из коробки?

Проблемы многопоточности:

  1. Data Race (гонка данных)
    Два потока одновременно читают/пишут одну память, хотя бы одна операция — запись.
    Приводит к неопределенному поведению, падениям, повреждению данных.
  2. Deadlock (взаимная блокировка)
    Поток A ждет ресурс, занятый B, а B ждет ресурс, занятый A.
    Приложение зависает навсегда.
  3. Livelock
    Потоки не заблокированы, но постоянно меняют состояние, не достигая прогресса.
  4. Priority Inversion
    Низкоприоритетный поток захватил ресурс, нужный высокоприоритетному. Высокий ждет, а средний — не дает низкому выполняться.
  5. Race condition
    Результат зависит от непредсказуемого порядка выполнения потоков.
  6. Memory corruption (из-за data race на ссылках).
  7. Thread explosion — создание слишком многих потоков (дорого).

Способы работы с многопоточностью из коробки в Swift/iOS:

Низкоуровневые:

  • Thread — прямое создание потоков (не рекомендуется).
  • POSIX threads (pthread) — через C interop.

GCD (Grand Central Dispatch) — DispatchQueue:

  • Серийные и конкурентные очереди.
  • sync / async.
  • DispatchGroup, DispatchSemaphore, DispatchWorkItem, DispatchBarrier.
  • DispatchSource (мониторинг файлов, таймеров).

OperationQueue (более высокий уровень):

  • Operation и BlockOperation.
  • Зависимости между операциями.
  • Максимальное количество одновременных операций.
  • Отмена, приоритеты, KVO.

NSLock и производные:

  • NSLock, NSRecursiveLock, NSCondition, NSConditionLock.
  • os_unfair_lock (быстрее, но низкоуровнево).
  • @synchronized (только Obj-C).

Swift Concurrency (async/await, с iOS 15 / macOS 12):

  • async / await.
  • Task, TaskGroup.
  • Actor — для защиты состояния.
  • @MainActor — для UI.
  • @Sendable — для передачи замыканий между потоками.
  • AsyncStream, AsyncSequence.

Другие:

  • RunLoop — для событийного цикла (в основном на главном потоке).
  • Timer (с run loop).
  • DispatchIO, DispatchData.

Рекомендации:

  • Для UI — @MainActor или DispatchQueue.main.
  • Для защиты данных — actor (новый Swift) или серийная очередь.
  • Для сложных зависимостей — OperationQueue.
  • Для простых асинхронных задач — Task / async.

24. В чем отличие deadlock и livelock?

Deadlock (мертвая блокировка)

Определение: Ситуация, когда два или более потоков (процессов) блокируют друг друга, каждый ожидает освобождения ресурса, занятого другим, и ни один не может продолжить выполнение.

Условия возникновения (Coffman): все 4 должны выполняться:

  1. Взаимное исключение (ресурс занят только одним)
  2. Удержание и ожидание (поток держит ресурс и ждет другой)
  3. Нет вытеснения (ресурс нельзя отнять)
  4. Циклическое ожидание (цикл в графе зависимостей)

Пример:

let lockA = NSLock()
let lockB = NSLock()


// Поток 1
lockA.lock()
lockB.lock() // ждет lockB, но он у Потока 2


// Поток 2
lockB.lock()
lockA.lock() // ждет lockA, но он у Потока 1

Симптомы: Приложение полностью зависает. Потоки не потребляют CPU (ждут).

Livelock (оживленная блокировка)

Определение: Потоки не заблокированы, они активно выполняют действия, но эти действия не ведут к прогрессу — состояние постоянно меняется, но работа не выполняется.

Пример:

  • Два человека встречаются в коридоре, оба уступают дорогу в одну сторону, потом оба снова шагают, снова уступают — бесконечно.
  • В коде: два потока пытаются захватить два ресурса, видят конфликт, освобождают всё, ждут случайное время и повторяют.

Симптомы: Потоки активно используют CPU (100%), но результат не достигается.

Сравнение:


Состояние потоков
Deadlock Заблокированы (ждут)
Livelock Активны, выполняют код
Использование CPU
Deadlock Низкое (или 0)
Livelock Высокое
Выход без вмешательства
Deadlock Нет
Livelock Иногда (если случайные задержки)
Диагностика
Deadlock Легко (зависание)
Livelock Трудно (кажется, что работает)

Как избежать:

  • Deadlock: всегда захватывать блокировки в одинаковом порядке, использовать таймауты, избегать вложенных блокировок.
  • Livelock: добавлять случайные задержки, экспоненциальную задержку, или детерминированный арбитр.

25. Как соотносятся поток и очередь?

Поток (Thread)

  • Единица выполнения, планируемая операционной системой.
  • Имеет свой стек, регистры, состояние (running, waiting, etc.).
  • Может быть вытеснен в любой момент.
  • В одном процессе может быть много потоков.
  • Создание потока — дорогая операция (~мегабайт стека + накладные расходы).

Очередь (DispatchQueue)

  • Абстракция уровня GCD (Grand Central Dispatch).
  • Не является потоком.
  • Позволяет отправлять блоки кода (замыкания) для выполнения.
  • Может быть серийной (serial) — один поток обслуживает очередь.
  • Или конкурентной (concurrent) — несколько потоков из пула GCD.

Соотношение:

  • Одна серийная очередьодин поток (но не обязательно тот же самый всегда — GCD может переключать очередь на другой поток после блокировки, но в момент выполнения — один поток).
  • Одна конкурентная очередьмного потоков (из пула GCD, обычно 64 максимум).
  • Один поток может обслуживать много разных очередей (например, main thread обслуживает main queue и любые очереди, отправленные на main поток через async).
  • Очередь не привязана жестко к потоку — GCD может мигрировать блок между потоками (но не во время выполнения блока).

Пример:

let serialQueue = DispatchQueue(label: "my.serial")
serialQueue.async {
// этот блок может выполняться на потоке #3
}
serialQueue.async {
// а этот — на потоке #3 или другом, но последовательно
}

Главный поток и main queue:

  • DispatchQueue.main жестко привязана к главному потоку.
  • Это исключение.

Зачем нужны очереди, если есть потоки?

  • Управление потоками вручную сложно и опасно.
  • GCD предоставляет высокоуровневые примитивы (синхронизация, барьеры, группы).
  • Пулы потоков переиспользуются, экономя ресурсы.

26. Всегда ли на main thread задачи с main queue и всегда ли задачи с main queue попадают на main thread?

Ответ: ДА, всегда.

DispatchQueue.main — это специальная глобальная очередь, которая жестко привязана к главному потоку (main thread) приложения.

  • Любой блок, отправленный на main.async или main.sync, будет выполнен на главном потоке.
  • Если вы находитесь на главном потоке, то текущая очередь — это main (если вы не синхронно вызвали другую очередь, но тогда выполнение временно переключается, но исходный контекст остается main).

Почему так?

  • UI-фреймворк (UIKit, SwiftUI) требует, чтобы все операции с UI выполнялись на главном потоке.
  • main queue спроектирована как серийная очередь, работающая на главном потоке.

Проверка:

DispatchQueue.main.async {
print(Thread.isMainThread) // true
}

Исключения (нет, это миф):

Некоторые думают, что sync на main из main может переключить — но это deadlock, а не другой поток. Нет механизма, чтобы main queue использовала другой поток.

Итог:

  • Задачи из main queue → main thread
  • На main thread → задачи из main queue (или синхронно вызванные из других очередей, но тогда текущий контекст — main thread, но не main queue формально)

27. Что будет, если на DispatchQueue.main вызвать DispatchQueue.main.sync?

Deadlock (взаимная блокировка). Приложение зависнет навсегда.

Почему?

  1. Вы находитесь на главном потоке (main thread).
  2. sync означает: заблокируй текущий поток и не возвращай управление, пока переданный блок не выполнится.
  3. Блок должен выполниться на DispatchQueue.main.
  4. Но главный поток уже занят ожиданием (он не может выполнить блок, так как он сам же и ждет).
  5. Получается цикл: поток ждет себя же.

Код, который убьет приложение:

DispatchQueue.main.sync {
print("Never executed")
}

Что будет на практике:

  • Приложение перестает реагировать на касания.
  • UI не обновляется.
  • Через некоторое время watchdog (системный) убьет приложение (crash).

Исключения (не deadlock):

  • Если вызвать sync на main не из main (например, из фоновой очереди), то все работает:

DispatchQueue.global().async {
DispatchQueue.main.sync {
print("OK") // Блок выполнится на main, фоновый поток подождет
}
}

Как избежать:

  • Никогда не вызывайте .sync на той же очереди, на которой вы находитесь.
  • Для main queue — используйте только .async, если вы уже на main.
  • Если нужно гарантировать выполнение на main из любого места: DispatchQueue.main.async(не блокирует).

28. В чем отличия добавления на очередь sync и async?

sync (синхронное выполнение)

  • Блокирует текущий поток до завершения блока.
  • Возвращает управление только после выполнения блока.
  • Может использоваться для получения результата из блока.
  • Риск deadlock, если вызвать на текущей очереди.
  • Не создает дополнительных накладных расходов на планирование (блок выполняется сразу, если возможно).

async (асинхронное выполнение)

  • Не блокирует текущий поток.
  • Возвращает управление немедленно.
  • Блок выполнится позже (когда очередь дойдет до него).
  • Безопасен — никогда не приведет к deadlock на той же очереди.
  • Позволяет очереди самой решать, когда и на каком потоке выполнять блок.

Сравнительная таблица:

Блокировка текущего потока
sync Да
async Нет
Возврат управления
sync После блока
async Сразу
Deadlock на той же очереди
sync Да
async Нет
Получение возвращаемого значения
sync Да (через замыкание)
async Нет (нужен коллбэк или группа)
Использование
sync Для ожидания результата, синхронизации
async Для асинхронных задач, UI-обновлений

Пример с возвратом значения:

var result = 0
let queue = DispatchQueue.global()
queue.sync {
result = heavyComputation() // блокируем текущий поток, но получаем результат
}
print(result) // результат готов

Асинхронно так не сделать без дополнительных механизмов (DispatchGroup, коллбэк).

Пример с deadlock:

let queue = DispatchQueue.main
queue.sync { } // deadlock, если вызвано на main thread
queue.async { } // безопасно

Когда использовать sync:

  • Для синхронного доступа к общим данным (как замена блокировке).
  • Когда результат блока нужен немедленно.
  • Для обеспечения порядка выполнения (но лучше использовать серийную очередь с async).

Когда использовать async:

  • UI-обновления (всегда async с main).
  • Длительные операции (чтобы не блокировать UI).
  • Загрузка данных из сети.
  • Когда порядок выполнения не критичен или управляется очередью.

29. Mutex и семафор: в чем отличия?

Mutex (Mutual Exclusion)

  • Бинарный (значения 0 или 1).
  • Привязан к потоку-владельцу — только тот поток, который захватил mutex, может его освободить.
  • Поддерживает рекурсивный захват (если NSRecursiveLock).
  • Операции: lock() / unlock().
  • Блокирует поток, если mutex уже занят (ждет).

Семафор (Semaphore)

  • Счетчик (любое неотрицательное целое число).
  • Не привязан к потоку — любой поток может выполнить signal().
  • Операции: wait() (уменьшает счетчик, блокирует если 0) и signal() (увеличивает).
  • Может использоваться как бинарный (счетчик 1).
  • Позволяет ограничивать доступ к ресурсу N потокам одновременно.

Сравнение:


Тип
Mutex Бинарный
Семафор Счетчик (0..N)
Владелец
Mutex Есть (поток)
Семафор Нет
Рекурсивность
Mutex Может быть (RecursiveLock)
Семафор Нет (всегда рекурсивный по природе)
Освобождение
Mutex Только владельцем
Семафор Любым потоком
Производительность
Mutex Обычно быстрее
Семафор Чуть медленнее (из-за счетчика)
Применение
Mutex Защита критической секции
Семафор Ограничение числа одновременных доступов (пул соединений)

Примеры в Swift:

Mutex (NSLock):

let lock = NSLock()
lock.lock()
// критическая секция
lock.unlock()

Семафор (DispatchSemaphore):

let semaphore = DispatchSemaphore(value: 2) // 2 потока одновременно
semaphore.wait()
// ограниченный доступ
semaphore.signal()

Бинарный семафор vs Mutex:

  • Бинарный семафор (value = 1) может использоваться как mutex, но:
    Любой поток может сделать signal (даже не владелец) — это может сломать логику.
    Mutex проверяет владельца и предотвращает ошибочное освобождение.

Рекурсивный mutex:

let lock = NSRecursiveLock()
func recursive(_ n: Int) {
lock.lock()
if n > 0 { recursive(n-1) }
lock.unlock()
}
// Обычный NSLock завис бы на втором lock()

Когда что использовать:

  • Mutex — для защиты коротких критических секций.
  • Семафор — для ограничения количества одновременных операций (например, 10 загрузок одновременно).
  • RecursiveLock — для рекурсивных функций (но лучше избегать).

30. Что такое атомарные операции?

Атомарные операции — это операции чтения, записи или модификации памяти, которые выполняются неделимо с точки зрения других потоков. Ни один другой поток не может увидеть промежуточное состояние.

Свойства:

  • Неделимость — операция либо выполнилась полностью, либо не началась.
  • Без блокировок — не использует mutex (но может использовать аппаратные гарантии).
  • Обычно одна инструкция процессора (или несколько, но с гарантией).

Примеры атомарных операций:

  • atomic load — чтение значения
  • atomic store — запись значения
  • fetch_add — прочитать, прибавить, записать (все атомарно)
  • compare-and-swap (CAS) — если значение равно ожидаемому, заменить на новое

В Swift:

Стандартная библиотека не предоставляет атомарные типы напрямую. Но есть:

1. Swift Atomics (от Apple) — пакет swift-atomics:

import Atomics
var counter = ManagedAtomic<Int>(0)
counter.wrappingIncrement(ordering: .relaxed)

2. OSAtomic (устарело, но работает для совместимости):

import Darwin
var value: Int32 = 0
OSAtomicIncrement32(&value)

3. Через C++ interop:

cpp
std::atomic<int> counter;

Пример проблемы без атомарности:

var counter = 0
// Поток 1: counter += 1 (read, add, write)
// Поток 2: counter += 1
// Может стать 1 вместо 2 из-за гонки

Решение с атомарностью:

let counter = ManagedAtomic(0)
counter.wrappingIncrement(ordering: .relaxed) // атомарно

Ordering (модель памяти):

  • relaxed — только атомарность, без барьеров
  • acquire — последующие операции не переупорядочиваются до этой
  • release — предыдущие операции не переупорядочиваются после этой
  • sequentiallyConsistent — полная упорядоченность

Применение:

  • Счетчики
  • Флаги состояния
  • Lock-free структуры данных
  • Реализация once инициализации

Альтернативы (не атомарные, но проще):

  • NSLock — но с блокировками (дороже).
  • DispatchQueue — но с контекстом переключения.

31. В чем отличия параллелизма и конкуренции?

Эти термины часто путают. Их различие важно для понимания многопоточного программирования.

Конкурентность (Concurrency)

  • Логическая одновременность.
  • Несколько задач могут выполняться одновременно, но физически могут и не выполняться.
  • Это структура программы — способ организации кода.
  • Задачи могут чередоваться на одном ядре.
  • Пример: async/await, GCD с одним ядром.

Параллелизм (Parallelism)

  • Физическая одновременность.
  • Несколько задач действительно выполняются в один момент времени.
  • Это свойство выполнения — зависит от количества ядер.
  • Требует аппаратной поддержки (многоядерный процессор).
  • Пример: вычисления на всех ядрах CPU.

Ключевое различие (по Робу Пайку):

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.

Иллюстрация:

Конкурентность, но не параллелизм:

  • Одно ядро CPU.
  • Два потока переключаются через таймер.
  • Кажется, что они работают одновременно, но физически — последовательно.

Параллелизм:

  • Два ядра CPU.
  • Два потока работают строго одновременно.

Конкурентность + параллелизм:

  • Четыре ядра, 10 потоков — некоторые выполняются параллельно, некоторые ждут.

Примеры кода:

Конкурентность:

Task {
let data = await fetchData() // не блокирует поток, но может выполняться на том же ядре
await updateUI(data)
}

Параллелизм:

DispatchQueue.concurrentPerform(iterations: 100) { i in
heavyCompute(i) // выполняется на разных ядрах параллельно
}

Почему это важно?

  • Конкурентность — это дизайн (как разбить задачу на независимые части).
  • Параллелизм — это эффективность (как быстрее выполнить).

Ошибка:

"Мой код конкурентный, значит он параллельный" — нет. Для параллелизма нужно:

  1. Несколько ядер
  2. Задачи, которые действительно могут выполняться одновременно (нет глобальной блокировки)
  3. Планировщик, который их распределит

32. DispatchGroup — что такое?

DispatchGroup — механизм GCD для отслеживания завершения группы асинхронных задач. Позволяет дождаться выполнения нескольких блоков, отправленных на разные очереди.

Основные методы:

  1. enter() — увеличивает счетчик задач в группе (на 1).
  2. leave() — уменьшает счетчик. Когда счетчик становится 0 — группа завершена.
  3. notify(q:execute:) — выполняет блок, когда счетчик станет 0.
  4. wait() — синхронно блокирует текущий поток до завершения группы.

Простой пример:

let group = DispatchGroup()
let queue = DispatchQueue.global()

group.enter()
queue.async {
Thread.sleep(forTimeInterval: 1)
print("Task 1 done")
group.leave()
}

group.enter()
queue.async {
Thread.sleep(forTimeInterval: 2)
print("Task 2 done")
group.leave()
}

group.notify(queue: .main) {
print("All tasks done")
}
// Вывод: через 2 секунды "All tasks done"

Ручной enter/leave для асинхронных функций:

func fetchData(completion: @escaping () -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
completion()
}
}

let group = DispatchGroup()
group.enter()
fetchData {
print("Data loaded")
group.leave()
}
group.notify(queue: .main) {
print("Ready")
}

Синхронное ожидание (осторожно!):

group.wait() // блокирует текущий поток
// Не вызывать на main queue!

Использование с async (более удобно):

queue.async(group: group) {
// задача автоматически вызывает enter/leave
}

Пример загрузки нескольких изображений:

let group = DispatchGroup()
var images: [UIImage] = []

for url in urls {
group.enter()
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data, let image = UIImage(data: data) {
images.append(image)
}
group.leave()
}.resume()
}

group.notify(queue: .main) {
self.imageView.image = images.first
}

Важно:

  • Каждый enter() должен быть сбалансирован leave().
  • Вызов leave() больше раз, чем enter(), вызовет crash.
  • notify можно добавить несколько раз.
  • wait() с таймаутом: wait(timeout: .now() + 5).

33. Что такое ThreadPool, как живет поток, как поддерживается его жизнь в системе? Кто управляет жизненным циклом потока?

ThreadPool (пул потоков)

  • Набор переиспользуемых потоков.
  • Задачи не создают новые потоки, а берут готовые из пула.
  • Уменьшает накладные расходы на создание/уничтожение потоков.

Как живет поток (жизненный цикл):

  1. Создание:
    ОС выделяет память под стек (обычно 512 КБ — 1 МБ).
    Создается контекст потока (регистры, приоритет, группа).
    Вызывается функция-раннер (thread entry point).
  2. Ожидание (sleep):
    Поток не выполняет код, не потребляет CPU.
    Может быть разбужен по таймеру, событию, или при поступлении задачи.
  3. Выполнение (running):
    Поток активен на ядре CPU.
    Выполняет задачи из очереди пула.
  4. Блокировка (waiting):
    Поток ждет ресурс (mutex, I/O, сеть).
    ОС переключает на другой поток.
  5. Завершение:
    Поток завершает раннер-функцию.
    ОС освобождает стек и ресурсы.

Кто управляет жизненным циклом?

На уровне ОС: ядро (kernel) — планировщик потоков (scheduler). Оно решает, какой поток на каком ядре и как долго будет выполняться (вытесняющая многозадачность).

На уровне приложения:

  • GCD (Grand Central Dispatch) — управляет пулом потоков для своих очередей. Создает потоки при необходимости, уничтожает после простоя.
  • OperationQueue — использует GCD или свой пул.
  • Thread class — при создании Thread вы сами управляете запуском, но ОС управляет планированием.

Пул потоков в GCD:

  • GCD имеет глобальный пул потоков (обычно до 64).
  • Потоки создаются лениво, по мере необходимости.
  • После выполнения задачи поток не уничтожается, а переходит в сон (усыпляется) на несколько секунд. Если за это время приходит новая задача — просыпается.
  • Если поток долго не используется — ОС может его прибить, GCD создаст новый.

Код для наблюдения:

let queue = DispatchQueue.global()
queue.async {
print(Thread.current) // какой-то поток из пула
}

Параметры пула:

  • Минимальное/максимальное количество потоков.
  • Время простоя перед уничтожением.
  • Приоритеты потоков (quality of service).

Управление вручную (редко):

let thread = Thread {
// работа
}
thread.start()
thread.cancel()

Но лучше использовать GCD или OperationQueue.

34. Как реализовать потокобезопасный массив несколькими способами?

Потокобезопасный массив — доступ (чтение/запись) из нескольких потоков не вызывает data race.

Способ 1: Серийная очередь

class SafeArray<T> {
private var array = [T]()
private let queue = DispatchQueue(label: "safe.array")

func append(_ element: T) {
queue.async(flags: .barrier) {
self.array.append(element)
}
}

func get(at index: Int) -> T? {
queue.sync {
guard index < self.array.count else { return nil }
return self.array[index]
}
}
}

Способ 2: Конкурентная очередь + барьеры (оптимально)

class SafeArrayConcurrent<T> {
private var array = [T]()
private let queue = DispatchQueue(label: "safe.array", attributes: .concurrent)

func append(_ element: T) {
queue.async(flags: .barrier) {
self.array.append(element)
}
}

func get(at index: Int) -> T? {
queue.sync {
guard index < self.array.count else { return nil }
return self.array[index]
}
}
}

Способ 3: NSLock

class SafeArrayLock<T> {
private var array = [T]()
private let lock = NSLock()

func append(_ element: T) {
lock.lock()
array.append(element)
lock.unlock()
}

func get(at index: Int) -> T? {
lock.lock()
defer { lock.unlock() }
guard index < array.count else { return nil }
return array[index]
}
}

Способ 4: os_unfair_lock (быстрее)

import os

class SafeArrayUnfair<T> {
private var array = [T]()
private var lock = os_unfair_lock()

func append(_ element: T) {
os_unfair_lock_lock(&lock)
array.append(element)
os_unfair_lock_unlock(&lock)
}

func get(at index: Int) -> T? {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
guard index < array.count else { return nil }
return array[index]
}
}

Способ 5: Actor (Swift 5.5+, лучший)

actor SafeArrayActor<T> {
private var array = [T]()

func append(_ element: T) {
array.append(element)
}

func get(at index: Int) -> T? {
guard index < array.count else { return nil }
return array[index]
}
}
// Использование:
let safeArray = SafeArrayActor<Int>()
await safeArray.append(5)
let value = await safeArray.get(at: 0)

Способ 6: Семафор

class SafeArraySemaphore<T> {
private var array = [T]()
private let semaphore = DispatchSemaphore(value: 1)

func append(_ element: T) {
semaphore.wait()
array.append(element)
semaphore.signal()
}
}

Сравнение производительности (от быстрого к медленному):

  1. actor (современный, безопасный)
  2. os_unfair_lock
  3. NSLock
  4. Конкурентная очередь с барьерами
  5. Серийная очередь
  6. Семафор

Рекомендация:

  • В новом коде — actor.
  • В legacy коде — конкурентная очередь с барьерами (читается параллельно, пишет эксклюзивно).

35. Можно ли работать с UI с background потока?

Технически — можно, но категорически нельзя. Это приведет к неопределенному поведению, крешам, артефактам.

Почему нельзя?

  • UIKit не потокобезопасен.
  • Многие операции с UI обращаются к общим структурам (CALayer, event loop, display link).
  • Background поток может изменить состояние вью в момент, когда main поток его читает.

Что произойдет?

  • Случайные креши (EXC_BAD_ACCESS, assertion failures).
  • Неправильное отображение (мерцание, артефакты).
  • Необновленный UI (изменения не видны).
  • Зависания (deadlock с main потоком).

Исключения (очень редкие, но безопасные):

  • Создание UIImage из данных (без отображения).
  • Предварительный расчет текста (NSAttributedString bounding rect).
  • Подготовка изображений (масштабирование, обрезка) — но только если результат потом передается на main.
  • UIGraphicsImageRenderer — но сам рендеринг лучше на main или специальном контексте.

Проверка:

assert(Thread.isMainThread, "UI update on background thread")
// или
dispatchPrecondition(condition: .onQueue(.main))

Как правильно:

DispatchQueue.global().async {
let data = heavyNetworkCall()
DispatchQueue.main.async {
self.label.text = data // UI на main
}
}

Современный Swift (Swift Concurrency):

Task {
let data = await fetchData() // background
await MainActor.run {
self.label.text = data // main
}
}
// или
@MainActor
func updateUI() { ... }

Итог: НЕТ. Все UI-операции — только на главном потоке.

36. Всегда ли bounds.origin равен нулю и когда он не равен нулю?

Не всегда. bounds.origin может быть ненулевым.

По умолчанию:

  • У свежесозданного UIView bounds.origin = (0, 0).

Когда становится ненулевым:

1. Ручное изменение:

view.bounds.origin = CGPoint(x: 100, y: 200)

2. UIScrollView (самый частый случай):
При скролле contentOffset изменяет bounds.origin.
scrollView.bounds.origin отражает смещение содержимого.

scrollView.contentOffset = CGPoint(x: 50, y: 0) // bounds.origin станет (50,0)

  1. После трансформаций:
    При повороте/масштабировании frame меняется, но bounds.origin обычно остается нулевым, если не менять вручную.
  2. При анимациях (временные изменения).
  3. Внутри draw(_:):
    bounds.origin может указывать на область перерисовки.

Важно:

  • bounds — в собственной системе координат вью.
  • bounds.origin влияет на расположение сабвью.
  • Если изменить bounds.origin, все сабвью сдвинутся (как камера).

Пример:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.bounds.origin = CGPoint(x: 10, y: 20)
// Сабвью, добавленные с frame (0,0), отрисуются в (-10, -20) относительно view.

Итог:

  • Не всегда.
  • Ненулевой: при скролле, ручном изменении, некоторых анимациях.

37. В чем отличие frame и bounds?

Оба — CGRect, но разные системы координат.

Система координат
frame Родителя (superview)
bounds Своя (внутренняя)
Что определяет
frame Положение и размер в родителе
bounds Размер вью и внутренняя система координат
При изменении
frame Меняет положение и размер вью
bounds Меняет отображение сабвью

Подробно:

frame:

  • Относительно супервью (или окна, если нет родителя).
  • Используется Auto Layout, layoutSubviews, hitTest.
  • Включает в себя трансформации (поворот, масштабирование).

bounds:

  • Всегда в собственных координатах.
  • bounds.size обычно совпадает с frame.size (если нет трансформаций).
  • bounds.origin обычно (0,0), если не сдвигать.
  • Определяет внутреннюю систему для рисования и сабвью.

Пример:

let parent = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let child = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
parent.addSubview(child)

print(child.frame) // (50, 50, 100, 100) относительно parent
print(child.bounds) // (0, 0, 100, 100)

После поворота:

child.transform = CGAffineTransform(rotationAngle: .pi/4)
print(child.frame) // изменился (стал больше, сдвинулся)
print(child.bounds) // (0, 0, 100, 100) — НЕ изменился!

После изменения bounds.origin:

child.bounds.origin = CGPoint(x: 20, y: 20)
// Сабвью child (допустим, button) с frame (0,0) отрисуется в (-20, -20) относительно child

Когда что использовать:

  • frame — для расположения вью относительно родителя.
  • bounds — для рисования внутри вью и расположения сабвью.

38. Как увеличить область нажатия на кнопку?

Способ 1: Переопределить pointInside (самый чистый)

class ExtendedHitButton: UIButton {
var hitEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)

override func pointInside(_ point: CGPoint, with event: UIEvent?) -> Bool {
let extendedBounds = bounds.inset(by: hitEdgeInsets)
return extendedBounds.contains(point)
}
}

Способ 2: Обернуть в UIView (менее предпочтительно)

let container = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let button = UIButton(frame: CGRect(x: 20, y: 20, width: 60, height: 60))
container.addSubview(button)
// Но контейнер ловит нажатия, нужно перенаправлять.

Способ 3: Увеличить contentEdgeInsets (влияет на верстку)

button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
// Увеличивает frame кнопки, но сдвигает контент.

Способ 4: Использовать UIButton.Configuration (iOS 15+)

var config = UIButton.Configuration.plain()
config.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
button.configuration = config

Способ 5: Переопределить hitTest (для сложных иерархий)

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let extendedBounds = bounds.inset(by: UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -20))
if extendedBounds.contains(point) {
return self
}
return super.hitTest(point, with: event)
}

Лучшая практика:

class EnlargedButton: UIButton {
var enlargeInset: CGFloat = 10

override func pointInside(_ point: CGPoint, with event: UIEvent?) -> Bool {
let area = bounds.insetBy(dx: -enlargeInset, dy: -enlargeInset)
return area.contains(point)
}
}

Проверка:

  • Отрицательные инсеты расширяют область.
  • Положительные — сужают.
  • Не влияет на отображение, только на hitTest.

39. Отличия CALayer и UIView?

Сравнение:

Наследование
UIView UIResponder
CALayer NSObject
События
UIView Обрабатывает касания, жесты
CALayer Не обрабатывает
Responder Chain
UIView Участвует
CALayer Не участвует
Auto Layout
UIView Поддерживает
CALayer Не поддерживает (только через UIView)
Анимации
UIView Есть (обертка над CA)
CALayer Мощные, низкоуровневые
Отрисовка
UIView Через draw(_:)
CALayer Через display, draw(in:)
Производительность
UIView Тяжелее
CALayer Легче (напрямую работает с GPU)
Иерархия
UIView Управляет subviews
CALayer Управляет sublayers
Жизненный цикл
UIView layoutSubviews, didMoveToWindow
CALayer layoutSublayers, display

Взаимосвязь:

  • Каждый UIView имеет layer (свойство .layer).
  • CALayer — это "бэкенд" отрисовки, UIView — "фронтенд" с логикой и событиями.

Когда использовать CALayer напрямую:

  • Сложные анимации (трансформации, тени, маски).
  • Экономия памяти (если не нужны события).
  • Metal-рендеринг.
  • Фоновое рисование.

Когда использовать UIView:

  • Нужны касания, жесты.
  • Auto Layout.
  • Стандартные контролы.
  • Работа с ViewController.

Пример создания слоя без вью (но в иерархии вью):

let gradientLayer = CAGradientLayer()
gradientLayer.frame = view.bounds
view.layer.addSublayer(gradientLayer)

Производительность:

  • 1000 UIView — тормоза.
  • 1000 CALayer — нормально (но без событий).

Итог:

  • UIView = CALayer + события + Auto Layout + responder chain.
  • Используйте CALayer для анимаций и визуальных эффектов, UIView — для интерактивных элементов.

40. В чем отличия UIWindow от UIView, что такое UIWindow?

UIWindow — это:

  • Специальный подкласс UIView.
  • Корневой контейнер для всех вью приложения.
  • Обеспечивает связь между вью и физическим экраном.
  • Диспетчеризует события (касания, клавиатура, ориентация).

Отличия UIWindow от UIView:


Корневой
UIWindow Всегда корень иерархии
UIView Может быть вложенным
Связь с экраном
UIWindow Напрямую (через screen)
UIView Через окно
Ориентация
UIWindow Управляет поворотом
UIView Не влияет
События
UIWindow Первым получает и распределяет
UIView Получает от окна
Уровень (windowLevel)
UIWindow Есть (над/под другими окнами)
UIView Нет
keyWindow
UIWindow Только одно окно
UIView Не применимо
makeKeyAndVisible
UIWindow Да
UIView Нет

Что делает UIWindow:

  1. Предоставляет область для отображения.
  2. Маршрутизирует события касаний.
  3. Управляет анимацией переходов между ViewController.
  4. Содержит rootViewController.
  5. Управляет статус-баром и ориентацией.

Иерархия:

text

UIWindow (keyWindow)
└── rootViewController.view
└── другие вью

Пример создания окна вручную (редко, для мультиоконных приложений):

let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = MyViewController()
window.windowLevel = .alert // поверх всех
window.makeKeyAndVisible()

Несколько окон:

  • iOS поддерживает несколько окон (iPad, CarPlay, внешние дисплеи).
  • Но обычно одно keyWindow.

Итог:

  • UIWindow — это особое вью, корень иерархии, диспетчер событий и связи с экраном.
  • Без окна вью не отобразятся.

41. В чем отличия setNeedsLayout, layoutIfNeeded и layoutSubviews?

layoutSubviews()

  • Метод UIView, который реально пересчитывает фреймы сабвью.
  • Переопределяется для ручного позиционирования сабвью (когда Auto Layout не используется).
  • Не вызывать напрямую! Только через setNeedsLayout/layoutIfNeeded.
  • Вызывается автоматически системой в нужные моменты (поворот экрана, изменение bounds).

setNeedsLayout()

  • Асинхронно помечает вью как требующее перерасчета расположения.
  • Возвращает управление сразу.
  • Вызовет layoutSubviews в следующем цикле runloop.
  • Можно вызывать много раз подряд — layoutSubviews выполнится один раз.

layoutIfNeeded()

  • Синхронно проверяет флаг "needs layout".
  • Если флаг установлен — немедленно вызывает layoutSubviews.
  • Если не установлен — ничего не делает.
  • Используется, когда нужно гарантировать, что фреймы сабвью актуальны сейчас.

Пример:

view.setNeedsLayout()
// ... другой код
view.layoutIfNeeded() // теперь layoutSubviews выполнится сразу, если был помечен

Связь с Auto Layout:

  • При использовании Auto Layout вы не вызываете layoutSubviews напрямую.
  • Система сама вызывает layoutSubviews, который вызывает layoutSubviews у сабвью и применяет constraints.

Жизненный цикл:

  1. Вы меняете constraints или frame сабвью.
  2. Вызываете setNeedsLayout.
  3. В следующем цикле runloop система вызывает layoutSubviews (или layoutIfNeededсинхронно).
  4. В layoutSubviews пересчитываются фреймы (или применяются constraints).

Частая ошибка:

subview.frame = newFrame
view.layoutIfNeeded() // может не сработать, если не был setNeedsLayout
// Правильно: view.setNeedsLayout(); view.layoutIfNeeded()

42. Что такое intrinsicContentSize?

intrinsicContentSize — это естественный (внутренний) размер вью, основанный на его содержимом, без учета внешних ограничений.

Определение:

class MyView: UIView {
override var intrinsicContentSize: CGSize {
return CGSize(width: 100, height: 50)
}
}

Зачем нужно?

  • Auto Layout использует intrinsicContentSize, когда нет явных width/height constraints.
  • Позволяет вью "самоопределять" свой размер.

Примеры стандартных вью:

Вью / intrinsicContentSize
UILabel / Размер текста + шрифт
UIImageView / Размер изображения
UIButtonТекст / изображение + insets
UISwitch / Фиксированный (51x31)
UITextField / Высота шрифта + padding

Использование в Auto Layout:

let label = UILabel()
label.text = "Hello"
// Нет constraints на width/height
// Система использует intrinsicContentSize = ширина текста + высота шрифта

Когда переопределять:

  • Кастомные вью с фиксированным или вычисляемым размером.
  • Прогресс-бары, спиннеры.
  • Вью, размер которых зависит от их содержимого (например, обертка текста).

invalidateIntrinsicContentSize:

  • Если intrinsicContentSize изменился (например, изменился текст), нужно вызвать:

label.invalidateIntrinsicContentSize()

  • Система заново запросит intrinsicContentSize при следующем layout.

Связь с hugging/compression resistance:

  • Content Hugging Priority — насколько вью сопротивляется растяжению.
  • Compression Resistance Priority — насколько вью сопротивляется сжатию.
  • Оба работают с intrinsicContentSize.

Пример:

label.setContentHuggingPriority(.required, for: .horizontal)
// label не будет растягиваться шире intrinsic width

43. Жизненный цикл UIViewController.

Полная последовательность:

1. Инициализация

  • init(nibName:bundle:) или init(coder:) (из storyboard)
  • Не используйте init для настройки UI (вью еще нет).

2. loadView()

  • Создает корневой вью контроллера.
  • Не переопределять без крайней нужды (только для полностью кастомного вью).
  • Если переопределяете — не вызывать super.

3. viewDidLoad()

  • Вью загружена, но еще не добавлена в иерархию.
  • Лучшее место для настройки UI, добавления сабвью, загрузки данных, настройки делегатов.
  • Вызывается один раз за жизнь контроллера.

4. viewWillAppear(_:)

  • Вью вот-вот появится на экране.
  • Анимация появления еще не началась.
  • Здесь обновляют данные, которые могли измениться, пока вью не было видно.
  • Вызывается каждый раз перед появлением.

5. viewWillLayoutSubviews()

  • Перед первым layoutSubviews корневого вью (и при каждом изменении размеров).
  • Редко используется, но можно для изменения constraints до layout.

6. viewDidLayoutSubviews()

  • После layoutSubviews корневого вью.
  • Здесь можно делать финальные корректировки после того, как все фреймы рассчитаны.

7. viewDidAppear(_:)

  • Вью полностью виден на экране.
  • Анимация появления завершена.
  • Хорошее место для старта анимаций, запуска таймеров, аналитики.

8. viewWillDisappear(_:)

  • Вью вот-вот исчезнет.
  • Анимация исчезновения еще не началась.
  • Сохранить изменения, убрать клавиатуру, остановить проигрывание.

9. viewDidDisappear(_:)

  • Вью исчез с экрана.
  • Остановить таймеры, отписаться от уведомлений, очистить кэши.

10. deinit

  • Контроллер уничтожен.
  • ARC освободил память.
  • Проверить, что все уведомления и делегаты отписаны.

Дополнительные методы:

  • didReceiveMemoryWarning() — при нехватке памяти (освободить кэши, необязательные данные).
  • willMove(toParent:) / didMove(toParent:) — при добавлении/удалении контроллера как дочернего.

Порядок при повороте экрана:

viewWillLayoutSubviews
viewDidLayoutSubviews
(и так несколько раз)

Важно:

  • Не вызывать методы жизненного цикла вручную (кроме super).
  • При презенте/дисмиссе контроллера вызываются viewWillDisappear/viewDidDisappear у старого и viewWillAppear/viewDidAppear у нового.

44. Жизненный цикл приложения.

Управляется UIApplicationDelegate (в UIKit) или AppDelegate/SceneDelegate (в современных приложениях).

Состояния приложения:

  1. Not running — приложение не запущено или убито.
  2. Inactive — работает, но не получает события (например, при переключении, во время звонка).
  3. Active — нормальная работа, получает события.
  4. Background — в фоне, выполняет код ограниченное время.
  5. Suspended — в фоне, не выполняет код (может быть убито при нехватке памяти).

Методы делегата (AppDelegate):

  1. application(_:willFinishLaunchingWithOptions:)
    Самый первый метод.
    Приложение запускается, но еще не готово к отображению.
  2. application(_:didFinishLaunchingWithOptions:)
    Запуск завершен.
    Место для инициализации сервисов, настройки UI.
  3. applicationDidBecomeActive(_:)
    Приложение стало активным (после willEnterForeground или сразу после запуска).
    Возобновить таймеры, анимации, обновить UI.
  4. applicationWillResignActive(_:)
    Приложение скоро станет неактивным (например, звонок, свайп вверх).
    Сохранить состояние, остановить таймеры, замедлить анимации.
  5. applicationDidEnterBackground(_:)
    Приложение перешло в фон.
    Ограниченное время (~5 сек) для сохранения состояния. По истечении приложение может быть приостановлено.
    Освободить ресурсы, которые не нужны в фоне.
  6. applicationWillEnterForeground(_:)
    Приложение возвращается из фона на передний план (но еще не активно).
    Обновить данные, подготовить UI.
  7. applicationWillTerminate(_:)
    Приложение будет завершено (не для фоновых приложений, убитых системой).
    Сохранить финальное состояние.

Сценарии:

Запуск:

willFinishLaunching → didFinishLaunching → didBecomeActive

Звонок (уходит на фон):

willResignActive → didEnterBackground

Возврат из звонка:

willEnterForeground → didBecomeActive

Убийство приложения:

willTerminate (только если не suspended)

SceneDelegate (iOS 13+):

  • Для многоконных приложений (iPad, CarPlay).
  • Методы аналогичны, но для каждой сцены (окна).

Важно:

  • В фоне нельзя обновлять UI (приложение может быть убито).
  • Длительные задачи в фоне — через beginBackgroundTask.
  • Для загрузки контента в фоне — URLSession с background configuration.

45. Расскажи про responder chain.

Responder Chain — цепочка объектов, наследующих UIResponder, которые могут обрабатывать события (касания, движения, нажатия кнопок, редактирование).

Участники responder chain:

  • UIApplication
  • UIWindow
  • UIViewController
  • UIView
  • Любой кастомный UIResponder

Как работает:

  1. Начало — событие касания.
  2. hitTest находит самый глубокий UIView, содержащий точку касания.
  3. Это вью становится first responder для касания.
  4. Если вью не обрабатывает событие, оно передается его next responder:
    У вью: next = его супервью (или view controller, если он есть).
    У view controller: next = его родительский view controller (если есть) или окно.
    У окна: next = UIApplication.
    У UIApplication: next = app delegate (если он UIResponder).
    У app delegate: nil (конец).

Пример цепочки:

text

UIButton (самый глубокий)
→ UIView (супервью)
→ UIViewController (владелец вью)
→ UIWindow
→ UIApplication
→ AppDelegate

Зачем это нужно?

  • Позволяет обрабатывать события на разных уровнях.
  • Например, кнопка обрабатывает нажатие сама, а жест свайпа — родительское вью.
  • Удобно для глобальных действий (например, "shake" обрабатывает app delegate).

Методы для работы с responder chain:

  • next — следующий responder.
  • becomeFirstResponder() / resignFirstResponder() — для управления фокусом ввода.
  • canBecomeFirstResponder — можно ли стать первым.
  • touchesBegan/Moved/Ended/Cancelled — обработка касаний.

Пример переопределения:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// обработать сами
if !handleTouch(touches) {
// или передать дальше
next?.touchesBegan(touches, with: event)
}
}

События, которые идут по responder chain:

  • Касания (touches...)
  • Движения (акселерометр)
  • Удаленные события (пульт Apple TV)
  • Действия с клавиатуры
  • Редактирование текста

46. hitTest и pointInside: в чем отличия? Для чего нужны?

Оба метода используются для определения того, какое вью получит касание.

pointInside(point:with:event:) -> Bool

  • Проверяет, принадлежит ли точка текущему вью.
  • Не ищет сабвью.
  • По умолчанию проверяет, находится ли точка в bounds вью.
  • Можно переопределить для расширения/сужения области нажатия.

hitTest(_:with:) -> UIView?

  • Находит самый глубокий вью, который должен получить касание.
  • Вызывает pointInside для себя, если false — возвращает nil.
  • Если true — рекурсивно вызывает hitTest для всех сабвью (в обратном порядке добавления).
  • Возвращает первое ненулевое значение от сабвью, либо себя, если сабвью не подошли.

Отличия:


Что делает
pointInside Проверяет принадлежность точки вью
hitTest Находит целевое вью
Возвращает
pointInside Bool
hitTest UIView?
Рекурсивный
pointInside Нет
hitTest Да
Когда переопределят
ь
pointInside Расширение hit-области
hitTest Полная кастомная логика поиска

Пример переопределения pointInside (увеличение области):

override func pointInside(_ point: CGPoint, with event: UIEvent?) -> Bool {
let extendedBounds = bounds.insetBy(dx: -20, dy: -20)
return extendedBounds.contains(point)
}

Пример переопределения hitTest (игнорирование касаний):

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden || !self.isUserInteractionEnabled {
return nil
}
// Пропускаем касания сквозь себя
return super.hitTest(point, with: event) ?? nil
}

Типичное использование:

  1. Увеличение области нажатия — переопределить pointInside.
  2. Пропуск касаний сквозь вью — переопределить hitTest, вернуть nil для себя, но не для сабвью.
  3. Делегирование касаний другому вью — переопределить hitTest и вернуть другое вью.

Порядок вызовов:

text

touch → window.hitTest()
→ view.hitTest() (корневое)
→ view.pointInside()
→ для каждого сабвью в обратном порядке: subview.hitTest()

Важно:

  • hitTest игнорирует вью с isUserInteractionEnabled = false.
  • hitTest игнорирует вью с isHidden = true.
  • hitTest игнорирует вью с alpha < 0.01 (почти прозрачные).

47. Расскажи про Core Animation.

Core Animation — фреймворк для рендеринга и анимации графики на iOS/macOS. Работает на уровне CALayer, использует аппаратное ускорение (GPU).

Основные компоненты:

CALayer

  • Контейнер для визуального содержимого.
  • Свойства: position, bounds, transform, opacity, backgroundColor, cornerRadius, shadow*, border*.
  • Иерархия: слои могут содержать подчиненные слои.

CAAnimation

  • Базовый класс для анимаций.
  • Подклассы:
    CABasicAnimation — от одного значения к другому.
    CAKeyframeAnimation — массив ключевых значений.
    CATransition — переходы между слоями.
    CAAnimationGroup — группа анимаций.

CATransaction

  • Группирует несколько анимаций.
  • Позволяет задать длительность, функцию замедления, completion.

Типы анимаций:

1. Неявные (implicit)

  • Происходят автоматически при изменении некоторых свойств слоя (не у UIView, у CALayer).

layer.opacity = 0.5 // анимация по умолчанию (0.25 сек)

2. Явные (explicit)

  • Создаются вручную.

let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = 0
animation.toValue = 100
animation.duration = 1.0
layer.add(animation, forKey: "moveX")

Анимации UIView (обертка над CA):

UIView.animate(withDuration: 0.3) {
view.center.x = 100 // базовая анимация
}
UIView.animateKeyframes(withDuration: 1.0) { ... }

Особенности Core Animation:

  • Не меняет реальные значения свойств во время анимации (только presentation layer).
  • После анимации свойство возвращается к исходному значению, если не установить fillMode = .forwards + isRemovedOnCompletion = false.
  • Работает в отдельном процессе (не блокирует UI).

Presentation layer vs Model layer:

  • modelLayer — хранит целевые значения.
  • presentationLayer — показывает текущие анимационные значения.

let currentX = layer.presentation()?.position.x

Транзакции:

CATransaction.begin()
CATransaction.setAnimationDuration(0.5)
CATransaction.setCompletionBlock { print("done") }
layer.opacity = 0
CATransaction.commit()

Производительность:

  • Core Animation использует GPU для анимаций transform, opacity, position.
  • Некоторые свойства (shadowPath, cornerRadius с маской) могут быть тяжелыми.
  • Для лучшей производительности: shouldRasterize = true (но осторожно с памятью).

48. В чем отличия UICollectionView и UITableView?

UITableView

  • Одномерный список (только вертикальный скролл).
  • Одна колонка (секции и строки, но каждая строка на всю ширину).
  • Предопределенные стили ячеек (basic, subtitle, value1, value2).
  • Простые анимации вставки/удаления.
  • Встроенные заголовки/футеры секций.
  • UITableViewCell — специальный класс с textLabel, detailTextLabel, imageView.

UICollectionView

  • Двумерный (может быть вертикальным, горизонтальным, сеткой, круговым, любым).
  • Полная кастомизация лейаута через UICollectionViewLayout.
  • Ячейка — UICollectionViewCell (пустая, нужно добавлять сабвью).
  • Поддержка декоративных элементов (Supplementary Views — заголовки, футеры, любые другие).
  • Поддержка пинча, реорганизации ячеек (drag & drop).
  • Анимации обновлений — мощнее и гибче.

Сравнение:


Скролл
UITableView Только вертикальный
UICollectionView Любой (верт, гориз, сетка)
Компоновка
UITableView Фиксированная
UICollectionView Кастомный лейаут
Стили ячеек
UITableView Готовые
UICollectionView Только кастомные
Сложность
UITableView Простой
UICollectionView Сложный, но гибкий
Переиспользование
UITableView dequeueReusableCell
UICollectionView dequeueReusableCell (аналогично)
Декор. элементы
UITableView header/footer секций
UICollectionView Любые supplementary views
Анимации
UITableView Ограниченные
UICollectionView Полные
Drag & Drop
UITableView iOS 11+
iOS 11+ (более гибкий)

Когда что выбрать:

  • UITableView — простые списки, настройки, чаты, контакты (вертикальные).
  • UICollectionView — сетки фото, горизонтальные карусели, кастомные раскладки, Pinterest.

Пример создания коллекции с сеткой:

let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 100)
layout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)

Конвертация:

  • UITableView можно заменить UICollectionView с UICollectionViewFlowLayout с одной секцией и шириной ячейки равной ширине экрана.
  • Обратно — сложнее.

49. Что делать, если UITableView тормозит при скролле?

Причины и решения:

1. Тяжелые вычисления в cellForRowAt

  • Решение: вынести вычисления в фон.

DispatchQueue.global().async {
let processed = heavyWork(data)
DispatchQueue.main.async {
cell.textLabel?.text = processed
}
}

2. Не переиспользуются ячейки

  • Решение: всегда dequeueReusableCell(withIdentifier:).

3. Динамическая высота ячеек (Auto Layout)

  • Решение: задать estimatedRowHeight, не пересчитывать высоту для всех.

tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100

4. Прозрачность и закругления

  • Решение: избегать opaque = false, masksToBounds с закруглением.

cell.contentView.backgroundColor = .white
cell.layer.cornerRadius = 8
cell.layer.masksToBounds = true // тяжело, лучше у contentView

5. Загрузка изображений без кэша

  • Решение: использовать UIImageView с кэшем (Kingfisher, SDWebImage).

cell.imageView?.kf.setImage(with: url)

6. Сложная иерархия вью в ячейке

  • Решение: упростить, использовать drawRect только если нужно.

7. LayoutSubviews вызывается слишком часто

  • Решение: проверить constraints, убрать лишние.

8. Не кэшируются высоты

  • Решение: вручную кэшировать высоты, если они сложные.

var heightCache = [IndexPath: CGFloat]()
func tableView(_: heightForRowAt:) -> CGFloat {
if let h = heightCache[indexPath] { return h }
let h = calculateHeight()
heightCache[indexPath] = h
return h
}

9. Отображение градиентов и теней

  • Решение: использовать shouldRasterize для статических элементов.

cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale

10. Много текста с аттрибутами

  • Решение: предварительно вычислять NSAttributedString в фоне.

11. Дебаг: Core Animation инструменты

  • Instruments: Core Animation (Color Blended Layers, Color Offscreen-Rendered)

12. Оптимизация батчей

  • beginUpdates() / endUpdates() для групповых изменений.

Быстрая проверка:

  • Включить в симуляторе: Debug → Color Blended Layers (красное — плохо).
  • Color Offscreen-Rendered (желтое — плохо).

Итог:

  1. Профилировать (Instruments: Time Profiler, Core Animation).
  2. Упростить иерархию.
  3. Переиспользовать ячейки.
  4. Кэшировать высоты.
  5. Выносить тяжелое в фон.