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

200 вопросов по Swift

200 вопросов по Swift

Основы языка Swift

Вопрос

Что такое Swift?

Ответ

Swift — это современный, типобезопасный, компилируемый язык программирования, разработанный Apple для создания приложений под платформы iOS, macOS, watchOS, tvOS и Linux. Он сочетает в себе производительность C и простоту синтаксиса, поддерживает как процедурное, так и объектно-ориентированное программирование, а также функциональные парадигмы.


Вопрос

Как объявить константу и переменную в Swift?

Ответ

Константа объявляется с помощью ключевого слова let, переменная — с помощью var.

let pi = 3.14159 // константа
var counter = 0 // переменная

Тип данных выводится автоматически, но может быть указан явно:

let name: String = "Timur"
var age: Int = 31

Вопрос

В чём разница между let и var?

Ответ

let создаёт неизменяемое значение (константу), которое нельзя переназначить после инициализации.
var создаёт изменяемую переменную, значение которой можно менять в течение времени жизни.


Вопрос

Что такое опционалы (Optional) в Swift?

Ответ

Опционал — это тип, который может содержать либо значение, либо отсутствие значения (nil). Он используется для безопасной работы с потенциально отсутствующими данными. Обозначается добавлением ? к типу.

var name: String? = "Alice"
name = nil // допустимо

Вопрос

Как распаковать опционал?

Ответ

Существует несколько способов распаковки опционала:

  1. Принудительная распаковка (опасна, если значение nil):

    let unwrapped = optionalValue!
  2. Опциональная привязка (безопасна):

    if let unwrapped = optionalValue {
    print(unwrapped)
    }
  3. Оператор нулевого слияния:

    let result = optionalValue ?? "default"
  4. Guard-распаковка:

    guard let unwrapped = optionalValue else { return }

Вопрос

Что такое неявно распаковываемый опционал (Implicitly Unwrapped Optional)?

Ответ

Это опционал, который автоматически распаковывается при использовании. Обозначается как Type!. Используется, когда значение точно будет установлено до первого обращения к нему (например, в IBOutlet).

let assumedString: String! = "Hello"
print(assumedString) // без !

Если значение nil при обращении — произойдёт ошибка времени выполнения.


Вопрос

Что такое типы Int, Double, Float, Bool, String в Swift?

Ответ

Это базовые встроенные типы:

  • Int — целое число со знаком.
  • Double — число с плавающей точкой двойной точности (64 бита).
  • Float — число с плавающей точкой одинарной точности (32 бита).
  • Bool — логический тип со значениями true или false.
  • String — последовательность символов в Unicode.

Все они являются структурами (value types).


Вопрос

Как объявить массив и словарь в Swift?

Ответ

Массив:

var numbers = [1, 2, 3]
var emptyArray: [Int] = []

Словарь:

var capitals = ["Russia": "Moscow", "France": "Paris"]
var emptyDict: [String: String] = [:]

Типы могут выводиться автоматически или задаваться явно.


Вопрос

Можно ли изменять массив, объявленный через let?

Ответ

Нет. Если массив объявлен через let, он становится неизменяемым. Даже если элементы внутри — ссылочные типы, сам массив нельзя изменить (добавлять, удалять, заменять элементы).


Вопрос

Что такое кортеж (tuple) в Swift?

Ответ

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

let httpResponse = (200, "OK")
let (statusCode, statusMessage) = httpResponse
print(statusCode) // 200

Элементы могут иметь имена:

let person = (name: "Alice", age: 30)
print(person.name)

Управление памятью и ARC (Automatic Reference Counting)

Вопрос

Что такое ARC в Swift?

Ответ

ARC — это механизм автоматического подсчёта ссылок, используемый Swift для управления памятью объектов классов. Каждый раз, когда создаётся новая сильная (strong) ссылка на экземпляр класса, ARC увеличивает счётчик ссылок. Когда ссылка удаляется или выходит из области видимости, счётчик уменьшается. Как только счётчик достигает нуля, память освобождается.

ARC применяется только к ссылочным типам (классам), но не к значимым (struct, enum).


Вопрос

Почему в Swift используется ARC вместо сборщика мусора?

Ответ

ARC обеспечивает детерминированное и предсказуемое освобождение памяти без пауз, характерных для сборщиков мусора. Это особенно важно для мобильных платформ, где ресурсы ограничены, а производительность критична. Освобождение происходит сразу после того, как объект становится недостижимым.


Вопрос

Что такое сильная (strong) ссылка?

Ответ

Сильная ссылка — это обычная ссылка, которая удерживает объект в памяти. Пока существует хотя бы одна сильная ссылка на экземпляр класса, ARC не освобождает его память.

class Person {
var name: String
init(name: String) { self.name = name }
}

var person: Person? = Person(name: "Alice")
var anotherRef = person // strong reference

Здесь anotherRef — сильная ссылка, и объект Person остаётся в памяти.


Вопрос

Что такое слабая (weak) ссылка?

Ответ

Слабая ссылка не увеличивает счётчик ссылок и автоматически обнуляется при освобождении объекта. Объявляется с помощью ключевого слова weak. Тип слабой ссылки всегда должен быть опционалом.

class Apartment {
weak var tenant: Person?
}

Используется для предотвращения retain-циклов, например, между родителем и дочерним объектом.


Вопрос

Что такое несвязанная (unowned) ссылка?

Ответ

Несвязанная ссылка также не увеличивает счётчик ссылок, но не обнуляется при освобождении объекта. Она предполагает, что объект будет жить дольше текущего контекста. Если обратиться к уже освобождённому объекту через unowned, произойдёт ошибка времени выполнения.

Объявляется с помощью unowned:

class Customer {
var card: CreditCard!
}

class CreditCard {
unowned var owner: Customer
init(owner: Customer) { self.owner = owner }
}

Используется, когда можно гарантировать, что объект-владелец живёт дольше.


Вопрос

В чём разница между weak и unowned?

Ответ

  • weak — безопасна, всегда опциональна, автоматически становится nil при освобождении объекта.
  • unowned — не опциональна, не обнуляется, требует гарантии, что объект живёт дольше. При нарушении этой гарантии — краш.

Выбор зависит от жизненного цикла объектов:

  • Используйте weak, если объект может быть освобождён раньше.
  • Используйте unowned, если вы уверены, что этого не произойдёт.

Вопрос

Что такое retain-цикл (strong reference cycle)?

Ответ

Retain-цикл возникает, когда два или более объекта удерживают друг друга сильными ссылками, и ни один из них не может быть освобождён, потому что их счётчики ссылок никогда не достигают нуля.

Пример:

class A { var b: B? }
class B { var a: A? }

let x = A()
let y = B()
x.b = y
y.a = x // retain-цикл

Решение — заменить одну из ссылок на weak или unowned.


Вопрос

Как избежать retain-цикла в замыканиях?

Ответ

Используйте захватывающий список (capture list) с weak или unowned:

class NetworkManager {
var onComplete: (() -> Void)?

func fetchData() {
onComplete = { [weak self] in
guard let self else { return }
print("Data loaded")
}
}
}

Если self может быть освобождён до вызова замыкания — weak.
Если self точно живёт дольше — unowned.


Вопрос

Может ли ARC управлять памятью структур и перечислений?

Ответ

Нет. Структуры (struct) и перечисления (enum) — это значимые типы (value types). Они копируются при присваивании и передаче, и не используют ссылки. Поэтому ARC к ним не применяется. Память для них управляется стеком или встроенными механизмами копирования.


Вопрос

Что происходит при присваивании одного экземпляра класса другому?

Ответ

При присваивании экземпляра класса (ссылочного типа) создаётся новая сильная ссылка на тот же объект в куче. Сам объект не копируется.

let p1 = Person(name: "Bob")
let p2 = p1 // p2 — новая ссылка на тот же объект

Оба p1 и p2 указывают на один и тот же экземпляр. Изменения через одну ссылку видны через другую.


Функции и замыкания

Вопрос

Как объявить функцию в Swift?

Ответ

Функция объявляется с помощью ключевого слова func, за которым следуют имя, параметры в скобках и тело функции в фигурных скобках. Возвращаемый тип указывается после стрелки ->.

func greet(name: String) -> String {
return "Hello, \(name)!"
}

Если функция ничего не возвращает, тип возврата можно опустить или явно указать как Void или ().


Вопрос

Что такое внешние и внутренние имена параметров в Swift?

Ответ

В Swift каждый параметр функции имеет внешнее имя (используется при вызове) и внутреннее имя (используется внутри функции). По умолчанию первое внешнее имя опускается, а остальные совпадают с внутренними.

Пример:

func add(_ a: Int, to b: Int) -> Int {
return a + b
}

let result = add(5, to: 3) // "to" — внешнее имя второго параметра

Здесь _ означает, что у первого параметра нет внешнего имени. Можно задавать оба имени явно: func f(external internal: Type).


Вопрос

Можно ли задать значения по умолчанию для параметров функции?

Ответ

Да. Параметры функции могут иметь значения по умолчанию, указываемые через =:

func log(message: String, level: String = "INFO") {
print("[\(level)] \(message)")
}

log(message: "App started") // [INFO] App started
log(message: "Error occurred", level: "ERROR") // [ERROR] Error occurred

Параметры со значениями по умолчанию должны располагаться в конце списка.


Вопрос

Что такое вариадическая функция?

Ответ

Вариадическая функция принимает переменное количество аргументов одного типа. Обозначается троеточием ... после типа параметра. Внутри функции такой параметр становится массивом.

func sum(_ numbers: Int...) -> Int {
return numbers.reduce(0, +)
}

print(sum(1, 2, 3, 4)) // 10

Только один параметр в функции может быть вариадическим, и он должен быть последним.


Вопрос

Что такое вложенные функции?

Ответ

Вложенные функции — это функции, определённые внутри другой функции. Они имеют доступ к переменным внешней функции и видны только внутри неё.

func chooseFunction(shouldAdd: Bool) -> (Int, Int) -> Int {
func add(_ a: Int, _ b: Int) -> Int { return a + b }
func multiply(_ a: Int, _ b: Int) -> Int { return a * b }

return shouldAdd ? add : multiply
}

Здесь add и multiply — вложенные функции.


Вопрос

Что такое замыкание (closure) в Swift?

Ответ

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

Синтаксис:

{ (parameters) -> ReturnType in
statements
}

Пример:

let greet = { (name: String) -> String in
return "Hi, \(name)!"
}
print(greet("Alice")) // Hi, Alice!

Вопрос

Какие сокращения синтаксиса замыканий существуют в Swift?

Ответ

Swift поддерживает несколько сокращений:

  1. Вывод типов: если типы известны из контекста, их можно опустить.
  2. Неявный возврат: если тело состоит из одного выражения, return можно не писать.
  3. Сокращённые имена аргументов: $0, $1, $2 и т.д.
  4. Опускание круглых скобок, если замыкание — последний аргумент.

Пример:

let numbers = [1, 2, 3]
let doubled = numbers.map { $0 * 2 } // вместо { (n: Int) -> Int in return n * 2 }

Вопрос

Что такое экранирующее (escaping) замыкание?

Ответ

Замыкание является экранирующим, если оно вызывается после завершения функции, в которую оно передано. По умолчанию замыкания в Swift не экранируют. Чтобы разрешить экранирование, используется аннотация @escaping.

var completionHandlers: [() -> Void] = []

func performAsyncTask(completion: @escaping () -> Void) {
completionHandlers.append(completion)
}

Без @escaping компилятор выдаст ошибку, так как замыкание сохраняется за пределами времени жизни функции.


Вопрос

Что такое автозамыкание (autoclosure)?

Ответ

Автозамыкание — это замыкание, которое автоматически создаётся из выражения, переданного как аргумент. Используется для отложенного выполнения выражения. Объявляется с помощью @autoclosure.

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

assert(2 + 2 == 5, "Math is broken")

Здесь 2 + 2 == 5 оборачивается в замыкание автоматически. Автозамыкания всегда неэкранирующие.


Вопрос

Можно ли перегружать функции в Swift?

Ответ

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

func process(_ value: Int) { print("Integer: \(value)") }
func process(_ value: String) { print("String: \(value)") }

process(42) // Integer: 42
process("hello") // String: hello

Перегрузка работает и для методов, и для инициализаторов.


Перечисления и структуры

Вопрос

Что такое перечисление (enum) в Swift?

Ответ

Перечисление — это тип, который определяет общее множество связанных значений как одно целое. В Swift enum может содержать ассоциированные значения, методы, вычисляемые свойства и даже реализовывать протоколы.

enum CompassPoint {
case north, south, east, west
}

Каждый случай (case) представляет возможное значение типа CompassPoint.


Вопрос

Можно ли присваивать сырые (raw) значения перечислениям?

Ответ

Да. Сырые значения — это предопределённые константные значения (строки, символы или числа), которые можно присвоить каждому случаю перечисления. Все случаи должны иметь один и тот же тип сырых значений.

enum Planet: Int {
case mercury = 1, venus, earth, mars
}

let earth = Planet.earth
print(earth.rawValue) // 3

Если не указать значение явно, оно автоматически увеличивается на 1 от предыдущего.


Вопрос

Как инициализировать перечисление по сырым значениям?

Ответ

Используется фейлибельный инициализатор init?(rawValue:), который возвращает опционал:

enum HTTPStatus: Int {
case ok = 200, notFound = 404
}

let status = HTTPStatus(rawValue: 200) // Optional(.ok)
let invalid = HTTPStatus(rawValue: 999) // nil

Если переданное значение не соответствует ни одному случаю, инициализатор возвращает nil.


Вопрос

Что такое ассоциированные значения в перечислениях?

Ответ

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

enum Result {
case success(String)
case failure(Error)
}

let outcome = Result.success("Data loaded")

При использовании применяется сопоставление с образцом (switch или if case):

switch outcome {
case .success(let message):
print("Success: \(message)")
case .failure(let error):
print("Error: \(error)")
}

Вопрос

Что такое структура (struct) в Swift?

Ответ

Структура — это пользовательский составной тип, который объединяет данные и функциональность. В Swift структуры являются значимыми типами: при присваивании или передаче они копируются.

struct Point {
var x: Double
var y: Double

func distance(to other: Point) -> Double {
return sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y))
}
}

Структуры могут иметь свойства, методы, инициализаторы, расширения и реализовывать протоколы.


Вопрос

Можно ли изменять свойства структуры внутри её метода?

Ответ

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

struct Counter {
var count = 0

mutating func increment() {
count += 1
}
}

var c = Counter()
c.increment() // допустимо

Без mutating компилятор выдаст ошибку.


Вопрос

Есть ли у структур деинициализаторы (deinit)?

Ответ

Нет. Структуры не имеют деинициализаторов, потому что они не управляются через ARC и не размещаются в куче. Их память освобождается автоматически при выходе из области видимости.


Вопрос

Имеют ли структуры инициализатор по умолчанию?

Ответ

Да. Если структура не определяет собственные инициализаторы, Swift предоставляет инициализатор-член (memberwise initializer), который принимает значения для всех хранимых свойств.

struct Size {
var width: Double
var height: Double
}

let s = Size(width: 10.0, height: 20.0) // memberwise initializer

Если определён хотя бы один собственный инициализатор, инициализатор-член больше не предоставляется автоматически.


Вопрос

Можно ли наследовать структуру от другой структуры?

Ответ

Нет. Структуры в Swift не поддерживают наследование. Они могут только реализовывать протоколы и получать функциональность через расширения.


Вопрос

В чём принципиальное различие между struct и class в Swift?

Ответ

Основное различие — в семантике копирования:

  • struct — значимый тип: при присваивании или передаче создаётся копия.
  • class — ссылочный тип: при присваивании создаётся новая ссылка на тот же объект.

Дополнительно:

  • Только классы поддерживают наследование, деинициализаторы и ARC.
  • Структуры не могут быть weak или unowned, так как не используют ссылки.
  • Структуры безопасны для многопоточности по умолчанию благодаря копированию.

Классы и наследование

Вопрос

Что такое класс в Swift?

Ответ

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

class Vehicle {
var numberOfWheels = 0

func description() -> String {
return "A vehicle with \(numberOfWheels) wheels"
}
}

Вопрос

Как создать экземпляр класса?

Ответ

Экземпляр класса создаётся с помощью вызова инициализатора после имени класса:

let car = Vehicle()
car.numberOfWheels = 4

Если класс не определяет собственные инициализаторы, он автоматически получает инициализатор без параметров.


Вопрос

Что такое наследование в Swift?

Ответ

Наследование — это механизм, позволяющий одному классу (подклассу) наследовать свойства, методы и другие характеристики от другого класса (суперкласса). Подкласс может расширять или переопределять унаследованное поведение.

class Car: Vehicle {
override func description() -> String {
return "A car with \(numberOfWheels) wheels"
}
}

Ключевое слово override обязательно при переопределении.


Вопрос

Можно ли наследовать от структуры или перечисления?

Ответ

Нет. Наследование в Swift доступно только для классов. Структуры и перечисления не поддерживают наследование, но могут реализовывать протоколы.


Вопрос

Что такое инициализатор (init) в классе?

Ответ

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

class Person {
let name: String

init(name: String) {
self.name = name
}
}

Все неопциональные свойства должны быть инициализированы до завершения init.


Вопрос

Какие виды инициализаторов существуют в Swift?

Ответ

Существует три основных вида:

  1. Обозначенный инициализатор (designated initializer) — основной инициализатор класса, отвечающий за полную инициализацию.
  2. Удобный инициализатор (convenience initializer) — вторичный инициализатор, который должен вызывать обозначенный инициализатор того же класса.
  3. Фейлибельный инициализатор (failable initializer) — может вернуть nil, если инициализация невозможна (init?).

Пример удобного инициализатора:

class Temperature {
var celsius: Double

init(celsius: Double) {
self.celsius = celsius
}

convenience init(fahrenheit: Double) {
let celsius = (fahrenheit - 32) * 5 / 9
self.init(celsius: celsius)
}
}

Вопрос

Как работает цепочка инициализации при наследовании?

Ответ

При наследовании Swift требует соблюдения двух правил:

  1. Подкласс должен инициализировать все свои новые свойства до вызова инициализатора суперкласса.
  2. Инициализатор суперкласса вызывается после инициализации собственных свойств, но до любого обращения к self или вызова методов.

Это гарантирует, что объект полностью инициализирован сверху донизу.


Вопрос

Что такое деинициализатор (deinit)?

Ответ

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

class FileHandler {
var file: FileDescriptor?

deinit {
close(file)
print("File closed")
}
}

Деинициализаторы вызываются автоматически ARC и не принимают параметров.


Вопрос

Можно ли переопределять свойства в подклассе?

Ответ

Да, с помощью наблюдателей свойств (willSet/didSet) или переопределения вычисляемых свойств. Для переопределения используется ключевое слово override.

class Circle {
var radius: Double = 0.0
var area: Double { return .pi * radius * radius }
}

class ColoredCircle: Circle {
override var area: Double {
didSet {
print("Area recalculated")
}
}
}

Хранимые свойства нельзя переопределять напрямую, но можно добавить наблюдатели даже к унаследованным хранимым свойствам.


Вопрос

Что такое окончательный класс (final class)?

Ответ

Класс, помеченный как final, не может быть унаследован. Это применяется для предотвращения переопределения или наследования, когда это не требуется или нежелательно по соображениям безопасности или производительности.

final class Utility {
static func log(_ message: String) {
print(message)
}
}

Аналогично можно помечать отдельные методы или свойства как final, чтобы запретить их переопределение.


Протоколы и расширения

Вопрос

Что такое протокол (protocol) в Swift?

Ответ

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

protocol Drawable {
func draw()
var color: String { get set }
}

Любой тип может соответствовать протоколу, если реализует все его требования.


Вопрос

Как тип может соответствовать протоколу?

Ответ

Тип соответствует протоколу, если он реализует все его требования. Соответствие объявляется в строке определения типа через двоеточие:

struct Circle: Drawable {
var color: String = "red"

func draw() {
print("Drawing a circle in \(color)")
}
}

Соответствие также может быть добавлено позже с помощью расширения.


Вопрос

Может ли протокол наследовать другие протоколы?

Ответ

Да. Протокол может наследовать один или несколько других протоколов, объединяя их требования:

protocol Shape: Drawable {
var area: Double { get }
}

struct Square: Shape {
var color: String = "blue"
var side: Double

var area: Double { side * side }

func draw() {
print("Drawing a square")
}
}

Здесь Shape наследует Drawable, поэтому Square должен реализовать оба набора требований.


Вопрос

Что такое расширение (extension) в Swift?

Ответ

Расширение добавляет новые функции к существующему типу: методы, вычисляемые свойства, инициализаторы, протоколы и т.д. Расширения работают даже с типами, исходный код которых недоступен (например, Int, String).

extension Int {
var squared: Int { self * self }
}

print(5.squared) // 25

Расширения не могут добавлять хранимые свойства или наблюдатели за существующими свойствами.


Вопрос

Можно ли использовать расширение для реализации протокола?

Ответ

Да. Это распространённая практика — выносить соответствие протоколу в отдельное расширение для лучшей читаемости:

struct Point {
var x: Double
var y: Double
}

extension Point: CustomStringConvertible {
var description: String {
return "(\(x), \(y))"
}
}

Такой подход позволяет отделить основную логику типа от его адаптации под внешние интерфейсы.


Вопрос

Что такое протокол с ассоциированным типом (associatedtype)?

Ответ

Ассоциированный тип — это заполнитель внутри протокола, который конкретный тип заменяет своим собственным типом при реализации.

protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

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

struct IntStack: Container {
typealias Item = Int
var items: [Int] = []

mutating func append(_ item: Int) { items.append(item) }
var count: Int { items.count }
subscript(i: Int) -> Int { items[i] }
}

Swift часто выводит Item автоматически из контекста.


Вопрос

Можно ли использовать протокол как тип?

Ответ

Да, но с ограничениями. Протокол без Self или ассоциированных типов можно использовать как тип переменной, параметра или возвращаемого значения:

func display(_ drawable: Drawable) {
drawable.draw()
}

Если протокол содержит associatedtype или ссылается на Self, он становится «неполным типом» и не может использоваться напрямую как тип — только в обобщённых контекстах (<T: Protocol>).


Вопрос

Что такое условное соответствие протоколу?

Ответ

Условное соответствие означает, что тип соответствует протоколу только при выполнении определённого условия. Часто используется с обобщёнными типами.

extension Array: Equatable where Element: Equatable {
// реализация ==
}

Здесь массив становится Equatable, только если его элементы тоже Equatable.


Вопрос

Можно ли добавить инициализатор через расширение?

Ответ

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

  • Для структур и перечислений можно добавлять удобные инициализаторы.
  • Для классов можно добавлять только удобные инициализаторы, и они не могут вызывать обозначенные инициализаторы суперкласса.
  • Инициализаторы, добавленные через расширение, не могут быть обозначенные.

Пример:

extension Person {
convenience init(name: String) {
self.init(fullName: name, age: 0)
}
}

Вопрос

Что такое протокол-композиция?

Ответ

Протокол-композиция позволяет указать, что значение должно соответствовать сразу нескольким протоколам. Синтаксис: ProtocolA & ProtocolB.

func render(_ item: Drawable & Animatable) {
item.draw()
item.animate()
}

Это особенно полезно, когда нужно временно объединить требования без создания нового протокола.


Обобщённое программирование (Generics)

Вопрос

Что такое обобщения (generics) в Swift?

Ответ

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

Пример обобщённой функции:

func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}

Здесь T — заполнитель типа, который заменяется на реальный тип при вызове.


Вопрос

Как объявить обобщённую структуру?

Ответ

Обобщённая структура объявляется с параметром типа в угловых скобках:

struct Stack<Element> {
var items: [Element] = []

mutating func push(_ item: Element) {
items.append(item)
}

mutating func pop() -> Element? {
items.popLast()
}
}

При использовании указывается конкретный тип:

var stringStack = Stack<String>()
stringStack.push("Hello")

Вопрос

Можно ли накладывать ограничения на обобщённые типы?

Ответ

Да. Ограничения задаются с помощью ключевого слова where или прямо в списке параметров через двоеточие.

Пример с ограничением:

func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool {
return a == b
}

Или с where:

func merge<T>(_ array1: [T], _ array2: [T]) -> [T] where T: Comparable {
return (array1 + array2).sorted()
}

Ограничения гарантируют, что тип поддерживает нужные операции.


Вопрос

Что такое обобщённые протоколы?

Ответ

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

protocol Queue {
associatedtype Element
mutating func enqueue(_ element: Element)
mutating func dequeue() -> Element?
}

Это позволяет создавать интерфейсы, работающие с разными типами данных.


Вопрос

Можно ли использовать обобщения в расширениях?

Ответ

Да. Расширения могут быть обобщёнными и применяться к обобщённым типам с условиями.

extension Array where Element: Numeric {
var sum: Element {
reduce(0, +)
}
}

let numbers = [1, 2, 3, 4]
print(numbers.sum) // 10

Такое расширение доступно только для массивов числовых типов.


Вопрос

Что такое обобщённые подпротоколы?

Ответ

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

protocol IntegerQueue: Queue where Element == Int {
func peek() -> Int?
}

Теперь любой тип, соответствующий IntegerQueue, должен использовать Int как Element.


Вопрос

Как работает вывод типов в обобщённых функциях?

Ответ

Swift автоматически выводит обобщённый тип на основе переданных аргументов. Явное указание типа обычно не требуется.

let result = isEqual(5, 10) // T выводится как Int
let same = isEqual("a", "b") // T выводится как String

Если компилятор не может вывести тип однозначно, потребуется явное указание:

let explicit: Bool = isEqual<Int>(5, 5)

Вопрос

Можно ли иметь несколько обобщённых параметров?

Ответ

Да. Функция или тип может принимать несколько параметров типа:

func zipArrays<A, B>(_ first: [A], _ second: [B]) -> [(A, B)] {
return Array(zip(first, second))
}

let pairs = zipArrays([1, 2], ["a", "b"]) // [(1, "a"), (2, "b")]

Каждый параметр типа может иметь свои собственные ограничения.


Вопрос

Что такое обобщённые инициализаторы?

Ответ

Инициализатор может быть обобщённым, даже если сам тип не является таковым. Это позволяет создавать экземпляры на основе значений разных типов.

struct Wrapper {
let value: Any

init<T>(_ wrapped: T) {
self.value = wrapped
}
}

let w1 = Wrapper(42)
let w2 = Wrapper("text")

Такой подход часто используется в обёртках или адаптерах.


Вопрос

В чём преимущество обобщений перед использованием Any или AnyObject?

Ответ

Обобщения обеспечивают типобезопасность на этапе компиляции. При использовании Any теряется информация о типе, что требует приведения типов и увеличивает риск ошибок времени выполнения. Обобщения сохраняют точный тип и позволяют компилятору проверять корректность операций.


Ошибки и обработка исключений

Вопрос

Как в Swift обрабатываются ошибки?

Ответ

Swift использует механизм обработки ошибок на основе протокола Error. Функции, которые могут выбрасывать ошибки, помечаются ключевым словом throws. Вызов таких функций осуществляется с помощью try, а обработка — через do-catch.

enum NetworkError: Error {
case timeout
case invalidURL
}

func fetchData() throws -> String {
throw NetworkError.timeout
}

do {
let data = try fetchData()
print(data)
} catch NetworkError.timeout {
print("Request timed out")
} catch {
print("Unknown error: \(error)")
}

Вопрос

Что такое протокол Error?

Ответ

Error — это пустой протокол, который используется для маркировки типов как ошибок. Любой тип (обычно перечисление) может соответствовать этому протоколу и использоваться в механизме обработки ошибок.

struct CustomError: Error {
let message: String
}

Соответствие Error достаточно для участия в throw/catch.


Вопрос

В чём разница между try, try? и try!?

Ответ

  • try — стандартный способ вызова функции, которая может выбросить ошибку. Должен использоваться внутри do-catch или в другой throws-функции.
  • try? — преобразует ошибку в опционал: если ошибка возникла, результат — nil; если нет — обёрнутый результат.
  • try! — подавляет обработку ошибки и предполагает, что ошибка никогда не произойдёт. Если ошибка всё же возникает — происходит ошибка времени выполнения.

Пример:

let result1 = try? fetchData() // String?
let result2 = try! fetchData() // String (краш при ошибке)

Вопрос

Можно ли выбрасывать ошибки из инициализаторов?

Ответ

Да. Инициализаторы могут быть помечены как init? throws или init throws. В первом случае они могут вернуть nil (фейлибельный инициализатор), во втором — передать ошибку выше.

struct User {
let name: String

init(from input: String) throws {
if input.isEmpty {
throw UserError.emptyName
}
self.name = input
}
}

Вопрос

Что такое фейлибельный инициализатор с обработкой ошибок?

Ответ

Это инициализатор, объявленный как init?, который может завершиться как возвратом nil, так и выбросом ошибки. Однако на практике такие инициализаторы редко используют throws, потому что фейлибельность уже выражает частичную неудачу. Обычно выбирают либо init?, либо init throws, но не оба сразу.

Если всё же объявить init? throws, то при вызове нужно использовать try, а результат будет Optional<T>? — двойной опционал, что усложняет использование.


Вопрос

Как создать собственную ошибку?

Ответ

Создайте перечисление (или структуру/класс), соответствующее протоколу Error, и определите случаи ошибок:

enum FileOperationError: Error {
case fileNotFound(String)
case permissionDenied
case diskFull
}

Затем используйте throw для генерации ошибки и catch для её обработки.


Вопрос

Можно ли перехватывать несколько типов ошибок в одном catch?

Ответ

Да. Можно использовать несколько блоков catch с разными шаблонами или использовать условия:

do {
try performTask()
} catch FileError.notFound {
print("File not found")
} catch FileError.permissionDenied {
print("Access denied")
} catch is NetworkError {
print("Network issue")
} catch {
print("Unexpected error: \(error)")
}

Swift проверяет блоки catch сверху вниз и выполняет первый подходящий.


Вопрос

Поддерживает ли Swift автоматическое освобождение ресурсов при ошибке (аналог finally)?

Ответ

Swift не имеет конструкции finally, но предоставляет defer. Блок defer выполняется при выходе из текущей области видимости — независимо от того, произошла ошибка или нет.

func processFile() throws {
let file = openFile()
defer {
close(file)
}
try readFile(file)
// close(file) вызовется даже при ошибке
}

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


Вопрос

Можно ли игнорировать ошибку без обработки?

Ответ

Да, с помощью _ = try? ... или try? _ = ..., но это считается плохой практикой, так как скрывает потенциальные проблемы. Явная обработка или документированное игнорирование предпочтительнее.


Вопрос

Что происходит, если ошибка выброшена в функции, не помеченной как throws?

Ответ

Компилятор выдаст ошибку. Только функции, помеченные как throws, могут выбрасывать ошибки напрямую. Если ошибка возникает внутри такой функции, она должна быть либо обработана (do-catch), либо передана выше через throws.


Потоки выполнения и конкурентность

Вопрос

Какие средства конкурентности предоставляет Swift?

Ответ

Начиная с Swift 5.5, язык включает встроенную поддержку асинхронного программирования через ключевые слова async/await, а также акторов (actor) для безопасной работы с общим состоянием. Ранее разработчики использовали Grand Central Dispatch (GCD) и Operation Queue.

Современный подход:

func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}

Task {
do {
let data = try await fetchData()
print("Loaded \(data.count) bytes")
} catch {
print("Failed: \(error)")
}
}

Вопрос

Что означает ключевое слово async?

Ответ

async помечает функцию как асинхронную — то есть она может приостанавливать своё выполнение, не блокируя текущий поток, пока ожидает завершения длительной операции (например, сетевого запроса или чтения файла). Такая функция может содержать точки приостановки (await).

Асинхронные функции вызываются только из других асинхронных контекстов или обёрнутых в Task.


Вопрос

Что делает ключевое слово await?

Ответ

await указывает точку приостановки внутри асинхронной функции. При достижении этой точки выполнение приостанавливается до тех пор, пока вызванная асинхронная операция не завершится. После этого выполнение возобновляется с полученным результатом или ошибкой.

let user = try await fetchUser(id: 123)

Если функция выбрасывает ошибку, используется try await.


Вопрос

Что такое Task в Swift?

Ответ

Task — это структура, представляющая отдельную асинхронную операцию. Она запускает выполнение асинхронного кода из синхронного контекста (например, из viewDidLoad).

let task = Task {
let result = await someAsyncFunction()
print(result)
}

Можно отменить задачу: task.cancel(). Внутри задачи можно проверить отмену через Task.isCancelled.


Вопрос

Что такое актор (actor)?

Ответ

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

actor Counter {
private var value = 0

func increment() {
value += 1
}

func getValue() -> Int {
return value
}
}

let counter = Counter()
Task {
await counter.increment()
let current = await counter.getValue()
}

Все методы актора по умолчанию изолированы (actor-isolated).


Вопрос

Можно ли вызывать асинхронную функцию из синхронного кода без Task?

Ответ

Нет. Асинхронные функции нельзя вызвать напрямую из синхронного контекста. Для запуска требуется либо находиться внутри другой async-функции, либо обернуть вызов в Task, Task.detached или использовать MainActor.run для главного потока.


Вопрос

Что такое MainActor?

Ответ

MainActor — это глобальный актор, который гарантирует выполнение кода в главном потоке (UI-потоке). Используется для обновления интерфейса.

@MainActor
func updateLabel(text: String) {
label.text = text
}

// Или внутри Task:
Task { @MainActor in
label.text = "Updated"
}

Функции, помеченные @MainActor, могут вызываться только из главного актора или асинхронно через await.


Вопрос

Как отменить асинхронную операцию?

Ответ

Используйте Task.cancel(). Внутри асинхронной функции можно периодически проверять Task.isCancelled или использовать try Task.checkCancellation(), который выбрасывает ошибку CancellationError, если задача отменена.

func longRunningOperation() async throws -> String {
for i in 0..<100 {
try Task.checkCancellation()
try await Task.sleep(for: .milliseconds(10))
}
return "Done"
}

Вопрос

Что такое async let?

Ответ

async let позволяет запустить несколько асинхронных операций параллельно и дождаться их результатов в одной точке.

func fetchBoth() async throws -> (Data, Data) {
async let first = fetchData(from: url1)
async let second = fetchData(from: url2)
return try await (first, second)
}

Обе операции начинаются немедленно и выполняются одновременно. Ожидание происходит только при первом использовании await.


Вопрос

Поддерживает ли Swift структурную конкурентность?

Ответ

Да. Начиная с Swift 5.5, компилятор включает систему проверки изоляции акторов на этапе компиляции (actor isolation checking). Она гарантирует, что данные, принадлежащие актору, не будут доступны из других потоков без асинхронного вызова, тем самым предотвращая гонки данных на уровне языка.

Это часть более широкой модели, известной как структурная конкурентность (structured concurrency), которая обеспечивает чёткую иерархию задач и безопасное управление жизненным циклом.


Работа с памятью, производительность и оптимизация

Вопрос

Как Swift управляет памятью для значимых типов (struct, enum)?

Ответ

Значимые типы в Swift управляются через семантику копирования. При присваивании или передаче в функцию создаётся независимая копия. Память для таких типов обычно размещается в стеке, что обеспечивает быстрое выделение и освобождение. ARC к ним не применяется.

Swift использует оптимизацию Copy-on-Write (COW) для стандартных коллекций (Array, String, Dictionary): копирование данных происходит только при изменении, если на буфер есть несколько ссылок.


Вопрос

Что такое Copy-on-Write (COW) и как он работает в Swift?

Ответ

Copy-on-Write — это стратегия управления памятью, при которой несколько переменных могут совместно использовать одни и те же данные до тех пор, пока ни одна из них не попытается изменить их. В момент изменения создаётся копия.

В Swift COW реализован для стандартных коллекций. Например:

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

Это снижает накладные расходы на копирование и повышает производительность.


Вопрос

Как избежать retain-циклов в замыканиях?

Ответ

Используйте захватывающий список (capture list) с weak или unowned:

class ViewController {
var button: UIButton!

func setupButton() {
button.tapHandler = { [weak self] in
self?.handleTap()
}
}

func handleTap() { /* ... */ }
}

Если self может быть освобождён до вызова замыкания — используйте weak. Если жизненный цикл гарантирует, что self живёт дольше — unowned.


Вопрос

Почему важно избегать сильных ссылок в делегатах?

Ответ

Делегаты часто создают retain-цикл: объект A владеет объектом B, а B хранит сильную ссылку на A через свойство delegate. Чтобы разорвать цикл, свойство делегата объявляется как weak.

protocol MyDelegate: AnyObject {
func didComplete()
}

class Worker {
weak var delegate: MyDelegate?
}

Протокол должен быть ограничен AnyObject, чтобы гарантировать, что делегат — ссылочный тип.


Вопрос

Как проверить утечки памяти в приложении на Swift?

Ответ

Используйте инструменты Xcode:

  • Debug Memory Graph: визуализирует граф объектов и показывает retain-циклы.
  • Instruments → Leaks: обнаруживает утечки памяти в реальном времени.
  • Allocations: отслеживает выделение и освобождение памяти.

Также можно добавлять логирование в deinit, чтобы убедиться, что объекты освобождаются вовремя.


Вопрос

Влияет ли использование final на производительность?

Ответ

Да. Пометка класса или метода как final позволяет компилятору применять оптимизации, такие как девиртуализация вызовов (прямой вызов вместо динамического диспетчеризирования через таблицу виртуальных функций). Это ускоряет выполнение и уменьшает размер кода.


Вопрос

Что происходит при частом создании и уничтожении объектов?

Ответ

Частое создание и уничтожение объектов увеличивает нагрузку на ARC и аллокатор памяти, что может привести к фрагментации памяти и снижению производительности. В критических участках кода рекомендуется использовать пулы объектов или повторное использование экземпляров.


Вопрос

Как уменьшить overhead от опционалов?

Ответ

Опционалы в Swift реализованы эффективно: они занимают столько же памяти, сколько базовый тип, благодаря использованию нулевых указателей или тегированных указателей. Однако чрезмерная вложенность (Optional<Optional<T>>) или частая распаковка могут снижать читаемость и немного влиять на производительность. Лучше проектировать API так, чтобы избегать глубокой вложенности и использовать if let/guard let для раннего выхода.


Вопрос

Почему стоит избегать принудительной распаковки (!)?

Ответ

Принудительная распаковка приводит к ошибке времени выполнения, если значение равно nil. Это нарушает принцип безопасности Swift и делает программу уязвимой к крашам. Использование безопасных методов (if let, guard let, ??) предпочтительнее.


Вопрос

Как влияет использование протоколов с associatedtype на производительность?

Ответ

Протоколы с associatedtype не могут использоваться как типы напрямую (только в обобщённых контекстах), что позволяет компилятору выполнять мономорфизацию — генерировать специализированный код для каждого конкретного типа. Это исключает динамическую диспетчеризацию и повышает производительность по сравнению с классами и виртуальными вызовами.


Работа с Cocoa и UIKit / SwiftUI

Вопрос

Как Swift взаимодействует с Objective-C и фреймворками Apple (UIKit, Foundation)?

Ответ

Swift полностью совместим с Objective-C через мост. Классы, методы и свойства, помеченные как @objc или соответствующие определённым критериям (наследование от NSObject, использование совместимых типов), автоматически доступны из Objective-C и наоборот. Это позволяет использовать UIKit, AppKit, CoreData и другие фреймворки Cocoa в Swift без дополнительных усилий.

Пример:

class MyViewController: UIViewController {
@objc func buttonTapped() {
// вызывается из Target-Action
}
}

Вопрос

Что такое @IBOutlet и @IBAction?

Ответ

@IBOutlet — это свойство, которое устанавливает связь между кодом и элементом интерфейса в Interface Builder (Storyboard/XIB). Оно должно быть weak (для UIView) и необязательным или неявно распаковываемым опционалом.

@IBAction — это метод, который вызывается при взаимодействии пользователя с элементом интерфейса (например, нажатии кнопки).

class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!

@IBAction func buttonPressed(_ sender: UIButton) {
label.text = "Tapped!"
}
}

Оба атрибута делают элементы видимыми для Interface Builder.


Вопрос

Почему @IBOutlet обычно объявляется как weak?

Ответ

Потому что представления (UIView и его подклассы) уже владеются своим супервью или контроллером через иерархию. Если бы @IBOutlet был strong, это создало бы retain-цикл: контроллер → strong → view → superview → ... → контроллер. Использование weak разрывает этот цикл.


Вопрос

Как обновлять UI из фонового потока?

Ответ

Все обновления пользовательского интерфейса должны выполняться в главном потоке. Для этого используется DispatchQueue.main.async или MainActor.

С использованием GCD:

DispatchQueue.global().async {
let result = performHeavyWork()
DispatchQueue.main.async {
self.label.text = result
}
}

С использованием async/await:

Task {
let result = await performHeavyWork()
await MainActor.run {
label.text = result
}
}

Вопрос

Что такое ViewDidLoad, ViewWillAppear, ViewDidAppear?

Ответ

Это методы жизненного цикла UIViewController:

  • viewDidLoad() — вызывается один раз после загрузки иерархии представлений в память. Подходит для инициализации.
  • viewWillAppear(_:) — вызывается каждый раз перед появлением вида на экране. Подходит для обновления данных.
  • viewDidAppear(_:) — вызывается после полного отображения вида. Подходит для запуска анимаций или аналитики.

Аналогичные методы существуют для скрытия: viewWillDisappear, viewDidDisappear.


Вопрос

Как работает UITableView в связке со Swift?

Ответ

UITableView требует реализации протоколов UITableViewDataSource и UITableViewDelegate. В Swift это делается через расширение или прямую реализацию в контроллере.

class TableViewController: UIViewController {
@IBOutlet var tableView: UITableView!
var items = ["A", "B", "C"]
}

extension TableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
}

Вопрос

Что такое Codable и как он используется в UIKit/SwiftUI?

Ответ

Codable — это композиция протоколов Encodable и Decodable, позволяющая легко сериализовать и десериализовать объекты в JSON или другие форматы. Широко используется для работы с сетевыми API и сохранением данных.

struct User: Codable {
var name: String
var age: Int
}

let json = """
{"name":"Alice","age":30}
""".data(using: .utf8)!

let user = try JSONDecoder().decode(User.self, from: json)
let data = try JSONEncoder().encode(user)

В UIKit/SwiftUI часто применяется для парсинга ответов от URLSession.


Вопрос

Как SwiftUI отличается от UIKit в подходе к UI?

Ответ

UIKit использует императивный подход: разработчик явно создаёт, настраивает и обновляет представления.
SwiftUI использует декларативный подход: описывается, каким должен быть интерфейс в зависимости от состояния, а система сама управляет обновлениями.

Пример SwiftUI:

struct ContentView: View {
@State private var text = "Hello"

var body: some View {
VStack {
Text(text)
Button("Update") { text = "Updated" }
}
}
}

Изменение @State автоматически перестраивает body.


Вопрос

Что такое @State, @Binding, @ObservedObject в SwiftUI?

Ответ

  • @State — хранилище состояния внутри представления. При изменении вызывает перерисовку.
  • @Binding — двусторонняя ссылка на значение, принадлежащее другому представлению или объекту.
  • @ObservedObject — ссылка на внешний объект, соответствующий ObservableObject. При изменении его @Published свойств представление обновляется.

Пример:

class UserData: ObservableObject {
@Published var name = "Alice"
}

struct ProfileView: View {
@ObservedObject var user: UserData
@Binding var isActive: Bool

var body: some View {
TextField("Name", text: $user.name)
Toggle("Active", isOn: $isActive)
}
}

Вопрос

Можно ли использовать UIKit-компоненты в SwiftUI?

Ответ

Да, с помощью UIViewRepresentable. Этот протокол позволяет обернуть UIView в SwiftUI-представление.

struct CustomMapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView()
}

func updateUIView(_ uiView: MKMapView, context: Context) {
// обновление при изменении состояния
}
}

Аналогично существует UIViewControllerRepresentable для контроллеров.


Тестирование и отладка

Вопрос

Как писать модульные тесты в Swift?

Ответ

Модульные тесты в Swift пишутся с использованием фреймворка XCTest. Тесты размещаются в отдельном таргете (обычно YourAppTests) и наследуются от XCTestCase. Каждый тест — это метод, начинающийся с test.

import XCTest
@testable import MyApp

class MathTests: XCTestCase {
func testAddition() {
XCTAssertEqual(2 + 2, 4)
}

func testStringEquality() {
XCTAssertTrue("hello" == "hello")
}
}

Ключевое слово @testable import позволяет тестировать внутренние (internal) функции и типы основного модуля.


Вопрос

Что такое XCTestAssert и какие его варианты существуют?

Ответ

XCTestAssert — это семейство макросов (на самом деле функций) для проверки условий в тестах. Основные из них:

  • XCTAssertTrue(_:) — проверяет, что выражение истинно.
  • XCTAssertFalse(_:) — проверяет, что выражение ложно.
  • XCTAssertEqual(_:_:) — проверяет равенство двух значений.
  • XCTAssertNotEqual(_:_:) — проверяет неравенство.
  • XCTAssertNil(_:) / XCTAssertNotNil(_:) — проверяют nil.
  • XCTAssertThrowsError(_:) — проверяет, что код выбрасывает ошибку.
  • XCTAssertNoThrow(_:) — проверяет, что код не выбрасывает ошибку.

Если утверждение ложно, тест завершается с ошибкой.


Вопрос

Как тестировать асинхронный код в Swift?

Ответ

Используйте XCTestExpectation или современный подход с async/await (доступен начиная с iOS 13+ и Xcode 13+).

Через expectation:

func testAsyncNetworkCall() {
let expectation = XCTestExpectation(description: "Data loaded")

fetchData { result in
switch result {
case .success(let data):
XCTAssertFalse(data.isEmpty)
case .failure:
XCTFail("Request failed")
}
expectation.fulfill()
}

wait(for: [expectation], timeout: 5.0)
}

Через async/await (в тесте, помеченном как async):

func testAsyncFunction() async throws {
let data = try await fetchDataAsync()
XCTAssertFalse(data.isEmpty)
}

Вопрос

Что такое мок-объект (mock) и как его использовать в тестах?

Ответ

Мок-объект — это поддельная реализация зависимости, используемая для изоляции тестируемого компонента. В Swift моки часто создаются через протоколы.

Пример:

protocol NetworkService {
func fetchUser(id: Int) async throws -> User
}

class MockNetworkService: NetworkService {
var userToReturn: User?
var shouldThrow = false

func fetchUser(id: Int) async throws -> User {
if shouldThrow { throw NetworkError.timeout }
return userToReturn ?? User(name: "Test")
}
}

// В тесте:
let mock = MockNetworkService()
mock.userToReturn = User(name: "Alice")
let viewModel = UserViewModel(network: mock)
// ... проверка поведения

Это позволяет контролировать поведение зависимости и тестировать различные сценарии.


Вопрос

Как отлаживать Swift-код в Xcode?

Ответ

Основные инструменты отладки в Xcode:

  • Точки останова (breakpoints) — приостанавливают выполнение на указанной строке.
  • LLDB-консоль — позволяет выполнять команды (po variable, expr, bt).
  • Debug Area — показывает значения переменных и стек вызовов.
  • View Debugger — визуализирует иерархию UI-элементов.
  • Memory Graph Debugger — помогает находить retain-циклы и утечки.

Также можно использовать print() или os_log для логирования.


Вопрос

Что делает команда po в LLDB?

Ответ

po (print object) — это команда LLDB, которая выводит описание объекта через его метод debugDescription или description. Работает с любыми типами Swift и Objective-C.

(lldb) po myArray
3 elements
- 0: "a"
- 1: "b"
- 2: "c"

Для простых значений можно использовать p (print), но po предпочтительнее для сложных объектов.


Вопрос

Как проверить покрытие кода тестами?

Ответ

В Xcode включите опцию Gather coverage data в схеме тестов:

  1. Product → Scheme → Edit Scheme.
  2. Выберите Test → Options.
  3. Установите галочку Code Coverage.
  4. Запустите тесты.
  5. Перейдите в Report Navigator → Coverage.

Там будет показан процент покрытия для каждого файла и выделены непротестированные строки.


Вопрос

Можно ли писать UI-тесты на Swift?

Ответ

Да. Xcode предоставляет XCUITest — фреймворк для автоматизации взаимодействия с интерфейсом приложения. UI-тесты запускают приложение как пользователь и эмулируют действия (нажатия, свайпы, ввод текста).

Пример:

class UITests: XCTestCase {
func testButtonTap() {
let app = XCUIApplication()
app.launch()

let button = app.buttons["Submit"]
XCTAssertTrue(button.exists)
button.tap()

let label = app.staticTexts["Success"]
XCTAssertTrue(label.waitForExistence(timeout: 5))
}
}

UI-тесты медленнее и хрупче модульных, но проверяют интеграцию компонентов.


Вопрос

Что такое setup() и tearDown() в XCTestCase?

Ответ

setUp() вызывается перед каждым тестовым методом и используется для инициализации общего состояния (создание объектов, настройка окружения).
tearDown() вызывается после каждого теста и используется для очистки (освобождение ресурсов, сброс состояния).

override func setUp() {
super.setUp()
database = InMemoryDatabase()
}

override func tearDown() {
database.reset()
super.tearDown()
}

Также существуют setUpWithError() и tearDownWithError() для асинхронных или выбрасывающих операций.


Вопрос

Как тестировать ошибки (throws) в Swift?

Ответ

Используйте XCTAssertThrowsError или XCTAssertNoThrow, либо пометьте тест как throws и используйте try.

Пример с перехватом ошибки:

func testInvalidInputThrows() {
XCTAssertThrowsError(try parseDate("")) { error in
XCTAssertTrue(error is DateParsingError)
}
}

Пример с ожиданием успеха:

func testValidInput() throws {
let date = try parseDate("2025-01-01")
XCTAssertNotNil(date)
}

Если try в таком тесте выбросит ошибку, тест завершится неудачей.


Продвинутые темы и особенности языка

Вопрос

Что такое Self в Swift?

Ответ

Self — это специальный тип, представляющий конкретный тип, который соответствует протоколу или наследует класс в данный момент. Он используется внутри протоколов и классов для ссылки на текущий тип.

В протоколе:

protocol Copyable {
func copy() -> Self
}

В классе:

class Vehicle {
func makeAnother() -> Self {
return type(of: self).init()
}
}

Self особенно полезен для обеспечения типобезопасного возврата экземпляров того же подкласса.


Вопрос

Что означает some перед типом возвращаемого значения?

Ответ

Ключевое слово some указывает на некоторый конкретный тип, соответствующий протоколу, но скрытый от вызывающего кода. Это часть механизма opaque result types (непрозрачных типов результата).

func makeShape() -> some Drawable {
return Circle(radius: 5)
}

Вызывающий код знает, что результат соответствует Drawable, но не знает, что это именно Circle. Это позволяет компилятору оптимизировать код и сохранять типобезопасность.


Вопрос

В чём разница между some Protocol и Protocol как типом возвращаемого значения?

Ответ

  • some Protocol означает один конкретный тип, реализующий протокол. Этот тип фиксирован при каждом вызове функции и известен компилятору.
  • Protocol (без some) допустим только если протокол не содержит Self или associatedtype. В этом случае возвращается существующий тип, упакованный в контейнер (existential container), что менее эффективно.

Если протокол имеет ассоциированные типы, использовать его напрямую как тип нельзя — только через some или обобщения.


Вопрос

Что такое экзистенциалы (existentials) в Swift?

Ответ

Экзистенциал — это коробка (контейнер), которая хранит значение любого типа, соответствующего заданному протоколу. Она позволяет работать с разными типами через единый интерфейс, но с накладными расходами на диспетчеризацию и выделение памяти.

let drawable: Drawable = Circle() // экзистенциал

Начиная с Swift 5.7, предпочтение отдаётся some Protocol (непрозрачным типам) вместо экзистенциалов для повышения производительности.


Вопрос

Что такое @propertyWrapper?

Ответ

@propertyWrapper — это механизм, позволяющий инкапсулировать логику доступа к свойству (валидация, трансформация, ленивая инициализация и т.д.) в отдельный тип.

Пример:

@propertyWrapper
struct TwelveOrLess {
private var value = 0
var wrappedValue: Int {
get { value }
set { value = min(12, max(0, newValue)) }
}
}

struct SmallRectangle {
@TwelveOrLess var width: Int
@TwelveOrLess var height: Int
}

При присваивании width = 15 фактическое значение станет 12.


Вопрос

Как работает @dynamicMemberLookup?

Ответ

@dynamicMemberLookup позволяет типу перехватывать обращения к несуществующим свойствам во время компиляции и обрабатывать их динамически через метод subscript(dynamicMember:).

@dynamicMemberLookup
struct JSON {
private let data: [String: Any]

init(_ data: [String: Any]) {
self.data = data
}

subscript(dynamicMember member: String) -> Any? {
return data[member]
}
}

let json = JSON(["name": "Alice", "age": 30])
print(json.name as? String) // Optional("Alice")

Используется в библиотеках для удобного доступа к данным (например, JSON, CSS).


Вопрос

Что такое @resultBuilder?

Ответ

@resultBuilder (ранее @_functionBuilder) — это механизм, позволяющий создавать DSL-подобный синтаксис, где тело функции или замыкания автоматически преобразуется в составной объект.

Используется в SwiftUI:

var body: some View {
VStack {
Text("Hello")
Button("Tap") { }
}
}

Здесь VStack использует ViewBuilder, который объединяет несколько View в один составной TupleView.

Собственный пример:

@resultBuilder
enum StringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined()
}
}

func buildMessage(@StringBuilder _ content: () -> String) -> String {
content()
}

let message = buildMessage {
"Hello"
" "
"World"
} // "Hello World"

Вопрос

Можно ли перегружать операторы в Swift?

Ответ

Да. Swift позволяет перегружать существующие операторы и создавать новые. Для этого объявляется функция с именем оператора.

Перегрузка существующего:

func + (left: Point, right: Point) -> Point {
Point(x: left.x + right.x, y: left.y + right.y)
}

Создание нового:

infix operator **

func ** (left: Double, right: Double) -> Double {
pow(left, right)
}

Операторы должны быть объявлены на глобальном уровне.


Вопрос

Что такое KeyPath и как он используется?

Ответ

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

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

let nameKeyPath = \Person.name
let person = Person(name: "Bob", age: 25)
print(person[keyPath: nameKeyPath]) // "Bob"

Используется в KVO (через @objc), SwiftUI (@Binding), сортировке (sorted(by: \.age)), и реактивных фреймворках.


Вопрос

Как работает деструктуризация кортежей и значений в Swift?

Ответ

Swift поддерживает деструктуризацию при присваивании и в параметрах циклов:

let point = (x: 10, y: 20)
let (x, y) = point
print(x) // 10

for (index, value) in ["a", "b"].enumerated() {
print("\(index): \(value)")
}

Также можно игнорировать части:

let (_, important) = fetchData()

Деструктуризация работает с кортежами, перечислениями с ассоциированными значениями и некоторыми коллекциями.


Работа с модулями, пакетами и сборкой

Вопрос

Что такое модуль в Swift?

Ответ

Модуль — это единый блок кода, который можно импортировать в другие части программы с помощью ключевого слова import. Каждый таргет в Xcode (приложение, фреймворк, библиотека) компилируется как отдельный модуль. Например, Foundation, SwiftUI, MyAppCore — всё это модули.

Файлы внутри одного модуля имеют доступ к друг другу в зависимости от уровней доступа (internal по умолчанию).


Вопрос

Какие уровни доступа существуют в Swift?

Ответ

Swift предоставляет пять уровней доступа:

  • open — самый открытый; позволяет наследовать классы и переопределять методы даже из других модулей.
  • public — доступен из других модулей, но без возможности наследования или переопределения.
  • internal — доступен только внутри текущего модуля (по умолчанию).
  • fileprivate — доступен только в пределах одного файла.
  • private — доступен только в пределах ближайшей области видимости (например, внутри одного типа или функции).

Пример:

public class APIManager {
private var apiKey: String
fileprivate func logRequest() { }
}

Вопрос

Что такое Swift Package Manager (SPM)?

Ответ

Swift Package Manager — это встроенный инструмент управления зависимостями и сборки проектов на Swift. Он использует манифест-файл Package.swift для описания пакета, его зависимостей, целей и платформ.

Пример Package.swift:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
name: "Networking",
platforms: [.iOS(.v15)],
products: [
.library(name: "Networking", targets: ["Networking"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0")
],
targets: [
.target(name: "Networking", dependencies: ["Algorithms"])
]
)

SPM интегрирован в Xcode и поддерживает локальные, удалённые и системные пакеты.


Вопрос

Как добавить зависимость через SPM в Xcode?

Ответ

  1. Откройте проект в Xcode.
  2. Перейдите в File → Add Package Dependencies…
  3. Введите URL репозитория (например, https://github.com/Alamofire/Alamofire.git).
  4. Выберите версию (точная, диапазон, ветка).
  5. Выберите таргеты, в которые нужно добавить зависимость.

Xcode автоматически загрузит пакет и добавит его в зависимости проекта.


Вопрос

Можно ли использовать Objective-C код в Swift-пакете?

Ответ

Нет, не напрямую. Swift Package Manager поддерживает только чистые Swift-модули. Если пакет содержит Objective-C, он не будет скомпилирован через SPM. Однако можно создать гибридный фреймворк в Xcode (через .xcodeproj) и использовать его как зависимость, но не как SPM-пакет.

Для совместимости рекомендуется переписывать Objective-C компоненты на Swift или предоставлять обёртки.


Вопрос

Что такое @_exported import?

Ответ

@_exported import — это атрибут, который делает импорт модуля «прозрачным»: любой код, импортирующий текущий модуль, автоматически получает доступ к экспортируемому модулю.

// Внутри MyFramework.swift
@_exported import Foundation

Теперь при import MyFramework становится доступен и Foundation. Это используется в обёртках и SDK для упрощения импорта.

Атрибут имеет андерскор, так как является недокументированным и может измениться в будущих версиях.


Вопрос

Как организовать внутренние зависимости между таргетами в одном пакете?

Ответ

В Package.swift каждая цель (target) может зависеть от других целей того же пакета через имя:

.targets([
.target(name: "Core", dependencies: []),
.target(name: "UI", dependencies: ["Core"]),
.testTarget(name: "CoreTests", dependencies: ["Core"])
])

Здесь UI зависит от Core, а тесты — от тестируемого модуля. Доступ между ними регулируется уровнями доступа (internal достаточно для одного пакета).


Вопрос

Что такое условная компиляция в Swift?

Ответ

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

#if DEBUG
print("Debug mode")
#endif

#if os(iOS)
import UIKit
#else
import AppKit
#endif

#if canImport(SwiftUI)
import SwiftUI
#endif

Также можно определять собственные флаги в настройках сборки Xcode (Other Swift Flags: -D MY_FLAG).


Вопрос

Как передать пользовательский флаг компиляции в Swift?

Ответ

В Xcode:

  1. Выберите таргет.
  2. Перейдите в Build Settings.
  3. Найдите Other Swift Flags.
  4. Добавьте -D FLAG_NAME.

Затем используйте в коде:

#if FLAG_NAME
print("Custom flag is active")
#endif

В SPM флаги можно задавать через swift build -Xswiftc -D -Xswiftc FLAG_NAME.


Вопрос

Что происходит при импорте модуля с одинаковым именем из разных источников?

Ответ

Конфликт имён модулей приведёт к ошибке компиляции. Swift требует, чтобы все модули в проекте имели уникальные имена. Чтобы избежать этого, при создании пакетов следует использовать уникальные имена (часто с префиксом компании или проекта, например, MyCompanyNetworking).

Если два пакета имеют одинаковое имя, Xcode не позволит добавить их одновременно.


Безопасность, надёжность и лучшие практики

Вопрос

Почему Swift считается типобезопасным языком?

Ответ

Swift обеспечивает типобезопасность за счёт строгой статической типизации, обязательной инициализации всех переменных перед использованием, отсутствия неявных преобразований между типами и явного управления опционалами. Компилятор проверяет корректность типов на этапе сборки, что предотвращает множество ошибок времени выполнения.


Вопрос

Как избежать ошибок, связанных с неинициализированными свойствами?

Ответ

Все неопциональные свойства класса или структуры должны быть инициализированы до завершения инициализатора. Swift гарантирует это на уровне компиляции. Для отложенной инициализации можно использовать:

  • Опциональные свойства (var name: String?)
  • Неявно распаковываемые опционалы (var view: UIView!), если значение точно будет установлено до первого использования
  • Ленивые свойства (lazy var), инициализируемые при первом обращении
class ViewController {
lazy var dateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .medium
return df
}()
}

Вопрос

Зачем использовать guard вместо if в некоторых случаях?

Ответ

guard обеспечивает ранний выход из функции при невыполнении условия, сохраняя основную логику на верхнем уровне вложенности. Это повышает читаемость и уменьшает «лесенку» вложенных условий.

func process(_ value: Int?) {
guard let value = value, value > 0 else { return }
print("Processing \(value)")
// остальная логика — без дополнительной вложенности
}

После guard развернутые значения доступны в той же области видимости.


Вопрос

Почему стоит избегать глобального состояния?

Ответ

Глобальное состояние усложняет тестирование, увеличивает связанность компонентов и создаёт риски гонок данных в многопоточной среде. Вместо этого рекомендуется передавать зависимости явно (через инициализаторы или параметры) или использовать контейнеры зависимостей с контролируемым жизненным циклом.


Вопрос

Как правильно обрабатывать опционалы в коллекциях?

Ответ

Избегайте хранения опционалов в коллекциях, если это не требуется семантически. Например, [String?] часто указывает на неудачный дизайн. Лучше фильтровать nil заранее или использовать непустые коллекции.

Если опционал необходим, используйте compactMap для извлечения ненулевых значений:

let optionalNames: [String?] = ["Alice", nil, "Bob"]
let validNames = optionalNames.compactMap { $0 } // ["Alice", "Bob"]

Вопрос

Что такое «nil-coalescing operator» и как его использовать правильно?

Ответ

Оператор нулевого слияния (??) возвращает левое значение, если оно не nil, иначе — правое.

let displayName = user.name ?? "Anonymous"

Правая часть должна быть того же типа, что и содержимое опционала. Избегайте побочных эффектов в правой части, так как она вычисляется всегда (нет ленивой оценки в старых версиях Swift; начиная с Swift 5.9 — поддерживается lazy evaluation при использовании замыкания).


Вопрос

Как обеспечить потокобезопасность в Swift?

Ответ

Для потокобезопасности следует:

  • Использовать акторы (actor) для совместно используемого изменяемого состояния.
  • Избегать глобальных переменных.
  • Не передавать ссылочные типы между потоками без синхронизации.
  • Использовать DispatchQueue.sync или async для защиты критических секций (в legacy-коде).
  • Предпочитать значимые типы (struct, enum), которые безопасны по умолчанию благодаря копированию.

Вопрос

Почему важно помечать протоколы делегатов как AnyObject?

Ответ

Протоколы делегатов должны быть ограничены AnyObject, чтобы гарантировать, что их могут реализовывать только классы. Это позволяет объявлять свойство делегата как weak, что необходимо для предотвращения retain-циклов.

protocol DataFetcherDelegate: AnyObject {
func didFetchData(_ data: Data)
}

class DataFetcher {
weak var delegate: DataFetcherDelegate?
}

Без AnyObject свойство не может быть weak, так как протокол может быть реализован структурой.


Вопрос

Как избежать «force unwrapping» в production-коде?

Ответ

Замените ! на безопасные альтернативы:

  • if let или guard let для распаковки с проверкой.
  • ?? для предоставления значения по умолчанию.
  • map/flatMap для цепочек опционалов.

Если значение должно быть ненулевым по логике программы, но компилятор не может это доказать, используйте precondition или assert с пояснением:

guard let url = URL(string: "https://example.com") else {
fatalError("Invalid URL constant")
}

Это делает причину краша явной и локализованной.


Вопрос

Какие принципы SOLID применимы в Swift?

Ответ

Все пять принципов SOLID применимы:

  • S (Single Responsibility): каждый тип решает одну задачу.
  • O (Open/Closed): расширение через протоколы и расширения, а не модификацию.
  • L (Liskov Substitution): подклассы должны быть взаимозаменяемы с суперклассами.
  • I (Interface Segregation): создавать узкоспециализированные протоколы вместо крупных.
  • D (Dependency Inversion): зависеть от абстракций (protocol), а не от конкретных реализаций.

Swift особенно хорошо поддерживает O, I и D через протоколы и расширения.


Сетевые операции и работа с данными

Вопрос

Как выполнить HTTP-запрос в Swift?

Ответ

Основной способ — использовать URLSession, встроенный фреймворк Apple. Можно выполнять запросы как синхронно (через замыкания), так и асинхронно (через async/await).

Пример с async/await:

func fetchJSON(from url: URL) async throws -> Data {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
return data
}

Пример с замыканием:

URLSession.shared.dataTask(with: url) { data, response, error in
// обработка результата
}.resume()

Вопрос

Как обработать ошибку сети в URLSession?

Ответ

Проверяйте три источника ошибок:

  1. Параметр error в замыкании — содержит системные ошибки (нет интернета, таймаут).
  2. Код статуса HTTP — даже при отсутствии error сервер может вернуть 404 или 500.
  3. Корректность данных — например, JSON может быть повреждён.

Пример:

if let error = error {
// сетевая ошибка
} else if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode != 200 {
// ошибка сервера
} else if let data = data {
// парсинг данных
}

Вопрос

Что такое Codable и как он связан с сетевыми запросами?

Ответ

Codable — это протокол, объединяющий Encodable и Decodable. Он позволяет автоматически преобразовывать структуры и классы в JSON и обратно с помощью JSONEncoder и JSONDecoder.

Пример:

struct User: Codable {
let id: Int
let name: String
}

let data = try await fetchJSON(from: userURL)
let user = try JSONDecoder().decode(User.self, from: data)

Это стандартный способ десериализации ответов API в Swift.


Вопрос

Как настроить заголовки в URLSession?

Ответ

Создайте URLRequest и установите свойство allHTTPHeaderFields или задавайте заголовки по одному:

var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"

let (data, _) = try await URLSession.shared.data(for: request)

Для повторного использования можно создать кастомную конфигурацию URLSession.


Вопрос

Можно ли отменить сетевой запрос?

Ответ

Да. При использовании замыканий сохраняйте ссылку на URLSessionDataTask и вызывайте cancel():

let task = URLSession.shared.dataTask(with: url) { ... }
task.resume()
// позже:
task.cancel()

При использовании async/await отмена происходит через механизм задач (Task):

let networkTask = Task {
try await fetchJSON(from: url)
}
// отмена:
networkTask.cancel()

Внутри функции можно проверять Task.isCancelled или вызывать try Task.checkCancellation().


Вопрос

Как обрабатывать загрузку файлов или изображений?

Ответ

Используйте тот же URLSession, но сохраняйте полученные данные в файл или декодируйте как изображение:

func loadImage(from url: URL) async throws -> UIImage {
let data = try await URLSession.shared.data(from: url).0
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}

Для больших файлов предпочтительно использовать downloadTask, который сохраняет данные во временный файл.


Вопрос

Что такое URLSessionConfiguration и зачем она нужна?

Ответ

URLSessionConfiguration определяет поведение сессии: политику кэширования, таймауты, cookie, прокси и другие параметры. Существует три типа:

  • .default — стандартная конфигурация с диск-кэшем и cookie.
  • .ephemeral — «приватный режим», без сохранения данных на диск.
  • .background — для фоновых загрузок, продолжает работу после закрытия приложения.

Пример:

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.httpCookieAcceptPolicy = .always
let session = URLSession(configuration: config)

Вопрос

Как реализовать повторные попытки (retry) при ошибке сети?

Ответ

Оберните вызов в цикл с ограничением по количеству попыток:

func fetchWithRetry(url: URL, maxRetries: Int = 3) async throws -> Data {
var lastError: Error?

for attempt in 1...maxRetries {
do {
return try await fetchJSON(from: url)
} catch {
lastError = error
if attempt < maxRetries {
try await Task.sleep(for: .seconds(pow(2, Double(attempt))))
}
}
}

throw lastError!
}

Можно расширить логику: повторять только при определённых ошибках (например, 5xx, таймаут).


Вопрос

Как защитить токены авторизации в приложении?

Ответ

Никогда не храните токены в UserDefaults или в открытом виде. Используйте Keychain — защищённое хранилище системы.

Пример с помощью библиотеки (например, KeychainAccess) или нативного API:

import Security

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "authToken",
kSecValueData as String: token.data(using: .utf8)!
]

SecItemAdd(query as CFDictionary, nil)

Keychain шифрует данные и привязывает их к приложению.


Вопрос

Как обрабатывать пагинацию в API?

Ответ

Реализуйте модель, которая отслеживает текущую страницу и URL следующей страницы. Часто API возвращает поле next или cursor.

Пример:

class PaginatedLoader<T: Codable> {
private var nextURL: URL?
private let session: URLSession

func loadNext() async throws -> [T] {
guard let url = nextURL ?? initialURL else { throw LoaderError.noURL }
let data = try await session.data(from: url).0
let response = try JSONDecoder().decode(PaginatedResponse<T>.self, from: data)
nextURL = response.next
return response.items
}
}

Такой подход инкапсулирует логику пагинации и легко тестируется.


Архитектурные паттерны и проектирование

Вопрос

Какие архитектурные паттерны используются в iOS-разработке на Swift?

Ответ

Наиболее распространённые паттерны:

  • MVC (Model-View-Controller) — стандартный паттерн Apple, где UIViewController часто становится «тяжёлым», объединяя логику представления и контроллера.
  • MVVM (Model-View-ViewModel) — разделяет логику отображения (View) и преобразования данных (ViewModel). Хорошо сочетается с SwiftUI и реактивными фреймворками.
  • VIPER (View-Interactor-Presenter-Entity-Router) — строгая модульная архитектура с чётким разделением ответственности, подходит для крупных команд и сложных проектов.
  • Clean Architecture — организует код в концентрические слои (Entities, Use Cases, Interface Adapters, Frameworks), обеспечивая независимость от внешних фреймворков.
  • Redux / Unidirectional Data Flow — состояние хранится централизованно, изменения происходят через действия и редюсеры (часто используется с библиотеками вроде ReSwift).

Выбор зависит от масштаба проекта, команды и требований к тестируемости.


Вопрос

В чём преимущество MVVM перед MVC?

Ответ

MVVM устраняет основной недостаток MVC — «массивный контроллер». Логика форматирования данных, обработка пользовательских действий и сетевые вызовы выносятся в ViewModel, который не зависит от UIKit/SwiftUI. Это делает код:

  • Тестируемым без запуска UI
  • Переиспользуемым между платформами
  • Более декларативным (особенно в SwiftUI)

View становится «глупой»: она только отображает данные и передаёт события.


Вопрос

Что такое ViewModel и как он работает?

Ответ

ViewModel — это объект, преобразующий данные модели в формат, удобный для отображения. Он не содержит ссылок на UI-элементы и не импортирует UIKit/SwiftUI (в идеале).

Пример:

class UserViewModel: ObservableObject {
@Published var displayName: String = ""
@Published var isLoading = false

private let userService: UserService

init(userService: UserService) {
self.userService = userService
}

func loadUser(id: Int) async {
isLoading = true
do {
let user = try await userService.fetch(id)
displayName = user.name.uppercased()
} catch {
displayName = "Error"
}
isLoading = false
}
}

В SwiftUI такой ViewModel легко привязывается через @StateObject.


Вопрос

Как реализовать внедрение зависимостей (Dependency Injection) в Swift?

Ответ

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

  1. Через инициализатор (рекомендуется):

    class UserManager {
    private let networkService: NetworkService
    init(networkService: NetworkService) {
    self.networkService = networkService
    }
    }
  2. Через свойство (property injection) — реже, для опциональных или изменяемых зависимостей.

  3. Через фабрику или контейнер — для сложных графов зависимостей.

Это упрощает тестирование (можно подставить мок) и повышает гибкость.


Вопрос

Что такое Coordinator (или Router) в архитектуре?

Ответ

Coordinator — это объект, отвечающий за навигацию между экранами. Он инкапсулирует логику переходов, освобождая ViewController от знания о других экранах.

Пример:

protocol Coordinator {
func start()
}

class AppCoordinator: Coordinator {
private let window: UIWindow

func start() {
let vc = MainViewController()
vc.onProfileTap = { [weak self] in
self?.showProfile()
}
window.rootViewController = UINavigationController(rootViewController: vc)
}

private func showProfile() {
let profileVC = ProfileViewController()
window.rootViewController?.present(profileVC, animated: true)
}
}

Это улучшает модульность и упрощает изменение потока навигации.


Вопрос

Как изолировать бизнес-логику от фреймворков?

Ответ

Следуйте принципу Clean Architecture: поместите чистую бизнес-логику в отдельный модуль (например, Core или Domain), который не импортирует UIKit, Foundation (кроме базовых типов) или сторонние библиотеки. Все внешние зависимости (сеть, база данных, файловая система) должны быть абстрагированы через протоколы.

Пример:

// В Core:
protocol UserRepository {
func save(_ user: User) throws
}

// В Infrastructure:
class CoreDataUserRepository: UserRepository {
func save(_ user: User) throws { /* реализация */ }
}

Такой подход позволяет легко заменять реализации и писать быстрые unit-тесты.


Вопрос

Что такое Use Case (Interactor)?

Ответ

Use Case — это объект, представляющий одну бизнес-операцию (например, «войти в систему», «загрузить список задач»). Он зависит от абстракций (репозиториев, сервисов) и не знает о UI или фреймворках.

class LoginUseCase {
private let authRepository: AuthRepository

init(authRepository: AuthRepository) {
self.authRepository = authRepository
}

func execute(email: String, password: String) async throws -> AuthToken {
return try await authRepository.login(email: email, password: password)
}
}

Use Case — центральный элемент Clean Architecture и VIPER.


Вопрос

Как обрабатывать ошибки на уровне архитектуры?

Ответ

Ошибки должны передаваться от нижних слоёв (сетевой, база данных) к верхним (ViewModel, View) через возвращаемые значения или замыкания. На уровне ViewModel или Presenter ошибки преобразуются в понятные пользователю сообщения или действия (показ алерта, повтор попытки).

Не используйте глобальные обработчики ошибок для бизнес-логики — это нарушает локальность и тестируемость.


Вопрос

Можно ли комбинировать архитектурные паттерны?

Ответ

Да. Часто в одном приложении используются разные подходы для разных частей. Например:

  • Основной экран — MVVM
  • Настройки — простой MVC
  • Платёжный модуль — VIPER для строгой изоляции

Главное — соблюдать единообразие внутри модуля и чётко разделять границы.


Вопрос

Как обеспечить тестируемость архитектуры?

Ответ

Следуйте трём правилам:

  1. Зависимости передаются извне (через DI).
  2. Логика отделена от UI (ViewModel, Use Case не знают о UIView).
  3. Все внешние сервисы абстрагированы через протоколы.

Тогда можно:

  • Подменить NetworkService на мок в тестах
  • Вызвать метод ViewModel и проверить изменение @Published свойств
  • Протестировать UseCase без запуска приложения

Это даёт высокое покрытие и уверенность в стабильности.


Работа с файловой системой и локальным хранилищем

Вопрос

Как получить путь к директории документов приложения в Swift?

Ответ

Используйте FileManager и метод urls(for:in:) с указанием домена .userDomainMask и типа директории .documentDirectory:

let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

Этот путь указывает на изолированную песочницу приложения, доступную только ему.


Вопрос

Как проверить, существует ли файл по заданному пути?

Ответ

Используйте метод fileExists(atPath:) у FileManager:

let fileURL = documentsURL.appendingPathComponent("data.txt")
if FileManager.default.fileExists(atPath: fileURL.path) {
print("File exists")
}

Можно также использовать isReadableFile(atPath:), isWritableFile(atPath:) для проверки прав.


Вопрос

Как записать данные в файл?

Ответ

Для Data используйте метод write(to:options:):

let text = "Hello, World!"
let data = text.data(using: .utf8)!
do {
try data.write(to: fileURL)
} catch {
print("Write failed: \(error)")
}

Для строк можно использовать write(toFile:atomically:encoding:):

try text.write(toFile: fileURL.path, atomically: true, encoding: .utf8)

Параметр atomically гарантирует целостность файла при сбое записи.


Вопрос

Как прочитать данные из файла?

Ответ

Используйте инициализатор Data(contentsOf:) или метод String(contentsOf:encoding:):

do {
let data = try Data(contentsOf: fileURL)
let text = String(data: data, encoding: .utf8)
print(text)
} catch {
print("Read failed: \(error)")
}

Всегда оборачивайте в do-catch, так как операции ввода-вывода могут выбрасывать ошибки.


Вопрос

Что такое UserDefaults и когда его следует использовать?

Ответ

UserDefaults — это простое хранилище «ключ-значение» для небольших объёмов данных: настройки, флаги, последние значения. Он не подходит для больших данных, чувствительной информации или сложных структур.

Пример:

UserDefaults.standard.set(true, forKey: "onboardingCompleted")
UserDefaults.standard.set(42, forKey: "score")

let completed = UserDefaults.standard.bool(forKey: "onboardingCompleted")
let score = UserDefaults.standard.integer(forKey: "score")

Данные сохраняются автоматически, но можно вызвать synchronize() (устаревший, обычно не требуется).


Вопрос

Почему UserDefaults не подходит для хранения токенов или паролей?

Ответ

UserDefaults сохраняет данные в plist-файле внутри песочницы приложения, но без шифрования. Эти данные могут быть извлечены при рутовании устройства или через архив IPA. Для конфиденциальных данных необходимо использовать Keychain, который обеспечивает аппаратное шифрование и строгий контроль доступа.


Вопрос

Как работает Keychain в iOS?

Ответ

Keychain — это защищённая система хранения, управляемая операционной системой. Каждая запись имеет уникальный идентификатор (kSecAttrAccount) и привязана к приложению (или группе приложений). Данные шифруются и недоступны другим приложениям.

Работа осуществляется через API Security framework:

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "authToken",
kSecValueData as String: token.data(using: .utf8)!
]

SecItemAdd(query as CFDictionary, nil)

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


Вопрос

Как удалить файл в Swift?

Ответ

Используйте метод removeItem(at:) у FileManager:

do {
try FileManager.default.removeItem(at: fileURL)
} catch {
print("Deletion failed: \(error)")
}

Если файла не существует, будет выброшена ошибка. Перед удалением можно проверить существование.


Вопрос

Можно ли создавать директории программно?

Ответ

Да. Используйте createDirectory(at:withIntermediateDirectories:attributes:):

let folderURL = documentsURL.appendingPathComponent("Cache")
do {
try FileManager.default.createDirectory(
at: folderURL,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
print("Directory creation failed: \(error)")
}

Параметр withIntermediateDirectories позволяет создавать всю цепочку родительских директорий, если они отсутствуют.


Вопрос

Как перечислить содержимое директории?

Ответ

Используйте contentsOfDirectory(at:includingPropertiesForKeys:options:):

do {
let contents = try FileManager.default.contentsOfDirectory(
at: documentsURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
for url in contents {
print(url.lastPathComponent)
}
} catch {
print("Listing failed: \(error)")
}

Метод возвращает массив URL, каждый из которых указывает на файл или поддиректорию.


Локализация и интернационализация

Вопрос

Как реализовать локализацию строк в приложении на Swift?

Ответ

Используйте файлы локализации Localizable.strings. Для каждого языка создаётся отдельная папка (например, en.lproj, ru.lproj), внутри которой находится файл с парами ключ-значение:

// Localizable.strings (en)
"welcome_message" = "Welcome!";
"greeting" = "Hello, %@!";

// Localizable.strings (ru)
"welcome_message" = "Добро пожаловать!";
"greeting" = "Привет, %@!";

В коде строки извлекаются через функцию NSLocalizedString:

let message = NSLocalizedString("welcome_message", comment: "Welcome screen title")
let greeting = String(format: NSLocalizedString("greeting", comment: ""), name)

Xcode автоматически выбирает нужный файл в зависимости от системного языка устройства.


Вопрос

Что означает параметр comment в NSLocalizedString?

Ответ

Параметр comment — это пояснение для переводчика, которое помогает понять контекст использования строки. Он не влияет на выполнение программы, но отображается в файлах .strings и инструментах локализации (например, в Xcode или внешних CAT-системах).

Пример:

NSLocalizedString("button.save", comment: "Label for the save button in settings screen")

Это особенно важно, когда один и тот же ключ может иметь разные значения в разных контекстах.


Вопрос

Как локализовать формат даты, чисел и валют?

Ответ

Используйте встроенные классы Foundation с учётом локали пользователя:

  • Дата: DateFormatter

    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    formatter.timeStyle = .short
    formatter.locale = Locale.current
    let dateString = formatter.string(from: Date())
  • Числа: NumberFormatter

    let numberFormatter = NumberFormatter()
    numberFormatter.numberStyle = .decimal
    numberFormatter.locale = Locale.current
    let numberString = numberFormatter.string(from: 1234567.89 as NSNumber)
  • Валюта: NumberFormatter с numberStyle = .currency

    numberFormatter.numberStyle = .currency
    numberFormatter.currencyCode = "USD"
    let priceString = numberFormatter.string(from: 99.99 as NSNumber)

Эти компоненты автоматически адаптируются под региональные настройки пользователя.


Вопрос

Можно ли изменить язык приложения программно?

Ответ

Нет, нельзя напрямую изменить язык, используемый NSLocalizedString, без перезапуска приложения или обходных манёвров. Система всегда использует язык, выбранный в настройках устройства, если только приложение не переопределяет механизм локализации.

Однако можно создать собственный менеджер локализации, который будет загружать строки из нужного .strings-файла вручную, игнорируя Locale.current. Это требует дополнительной работы и не рекомендуется Apple, так как нарушает ожидания пользователя.


Вопрос

Как локализовать Storyboard или XIB?

Ответ

Xcode поддерживает базовую локализацию интерфейса. При включении локализации для Storyboard создаются отдельные файлы для каждого языка (например, Main.storyboard (ru)), где можно перевести текст элементов (заголовки кнопок, метки и т.д.).

Однако этот подход трудно поддерживать в крупных проектах. Рекомендуется устанавливать текст программно в коде через NSLocalizedString, даже если элемент создан в Interface Builder.


Вопрос

Что такое plurals и как их обрабатывать в iOS?

Ответ

В отличие от Android, iOS не имеет встроенного механизма для множественных форм (plurals). Разработчик должен сам обрабатывать склонения и формы множественного числа в коде.

Пример для русского языка:

func pluralizedString(for count: Int) -> String {
let key: String
let remainder = count % 100
if (remainder > 10 && remainder < 15) {
key = "files_5"
} else {
switch remainder % 10 {
case 1: key = "files_1"
case 2, 3, 4: key = "files_2"
default: key = "files_5"
}
}
return String(format: NSLocalizedString(key, comment: ""), count)
}

Соответствующие строки в Localizable.strings:

"files_1" = "%d файл";
"files_2" = "%d файла";
"files_5" = "%d файлов";

Вопрос

Как проверить, поддерживает ли текущая локаль определённый формат?

Ответ

Можно использовать свойства Locale:

let locale = Locale.current
if locale.languageCode == "ru" {
// русскоязычный интерфейс
}

// Или проверить регион:
if locale.region?.identifier == "RU" {
// Россия
}

Также можно запрашивать доступные локали:

let available = Bundle.main.localizations

Вопрос

Что такое Base Internationalization?

Ответ

Base Internationalization — это режим в Xcode, при котором основной интерфейс (Storyboard/XIB) сохраняется в «базовой» локали (Base.lproj), а переводы хранятся отдельно в .strings-файлах для каждой локали. Это упрощает управление: дизайнер работает с одним файлом, а переводчики — только со строками.

Включается в Project Settings → Info → Localizations → Use Base Internationalization.


Вопрос

Как локализовать уведомления и ошибки?

Ответ

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

Пример:

let alert = UIAlertController(
title: NSLocalizedString("error.title", comment: ""),
message: NSLocalizedString("network.error.message", comment: ""),
preferredStyle: .alert
)

Никогда не показывайте сырые сообщения из Error напрямую.


Вопрос

Как тестировать локализацию в симуляторе?

Ответ

Можно изменить язык и регион в настройках симулятора (Settings → General → Language & Region).
Также можно запустить приложение с принудительной локалью через схему запуска в Xcode:

  1. Product → Scheme → Edit Scheme.
  2. Выберите Run → Arguments.
  3. В Launch Arguments добавьте:
    -AppleLanguages (ru)
    -AppleLocale ru_RU

Это временно переопределит локаль только для этого запуска.


Работа с графикой, анимациями и медиа

Вопрос

Как отобразить изображение в UIImageView?

Ответ

Используйте свойство image у UIImageView. Изображение можно загрузить из asset catalog или из файла:

let imageView = UIImageView()
imageView.image = UIImage(named: "logo") // из Assets.xcassets
// или
if let imageData = try? Data(contentsOf: imageURL),
let image = UIImage( imageData) {
imageView.image = image
}

Рекомендуется использовать UIImage(named:) для статических изображений — система автоматически выберет подходящее разрешение (@1x, @2x, @3x).


Вопрос

Как масштабировать изображение в UIImageView?

Ответ

Управляйте масштабированием через свойство contentMode:

  • .scaleToFill — растягивает без сохранения пропорций.
  • .scaleAspectFit — вписывает изображение в границы с сохранением пропорций (могут быть пустые поля).
  • .scaleAspectFill — заполняет всё пространство с сохранением пропорций (часть изображения может обрезаться).

Пример:

imageView.contentMode = .scaleAspectFit

Для корректного отображения также установите clipsToBounds = true, если используется .scaleAspectFill.


Вопрос

Как создать простую анимацию в UIKit?

Ответ

Используйте UIView.animate(withDuration:animations:):

UIView.animate(withDuration: 0.3) {
self.view.alpha = 0.0
self.button.center.x += 100
}

Можно добавить завершающий блок:

UIView.animate(withDuration: 0.3, animations: {
view.alpha = 0
}, completion: { _ in
view.removeFromSuperview()
})

Поддерживаются анимации свойств: frame, bounds, center, transform, alpha, backgroundColor.


Вопрос

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

Ответ

Используйте параметры usingSpringWithDamping и initialSpringVelocity:

UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.6,
initialSpringVelocity: 0.5,
options: [],
animations: {
view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
},
completion: nil
)
  • usingSpringWithDamping: от 0 (очень «пружинисто») до 1 (без колебаний).
  • initialSpringVelocity: начальная скорость (в единицах изменения за секунду).

Вопрос

Как работать с камерой и галереей в iOS?

Ответ

Используйте UIImagePickerController:

  1. Проверьте разрешения в Info.plist:

    <key>NSCameraUsageDescription</key>
    <string>Нужен доступ к камере для фото профиля</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>Нужен доступ к фото для выбора аватара</string>
  2. Реализуйте делегат:

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func selectImage() {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary // или .camera
present(picker, animated: true)
}

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
imageView.image = image
}
picker.dismiss(animated: true)
}
}

Вопрос

Как воспроизвести звук в приложении?

Ответ

Для коротких звуков (< 30 сек) используйте AVAudioPlayer или системный API AudioServicesPlaySystemSound.

Пример с AVAudioPlayer:

import AVFoundation

var audioPlayer: AVAudioPlayer?

func playSound() {
guard let url = Bundle.main.url(forResource: "beep", withExtension: "mp3") else { return }
do {
audioPlayer = try AVAudioPlayer( s: url)
audioPlayer?.play()
} catch {
print("Cannot play sound: \(error)")
}
}

Не забудьте запросить разрешение на микрофон, если записываете звук.


Вопрос

Как отобразить видео в приложении?

Ответ

Используйте AVPlayerViewController из фреймворка AVKit:

import AVKit

func playVideo(from url: URL) {
let player = AVPlayer(url: url)
let controller = AVPlayerViewController()
controller.player = player

present(controller, animated: true) {
player.play()
}
}

Для встраивания видео в интерфейс используйте AVPlayerLayer внутри кастомного UIView.


Вопрос

Как создать круглый аватар из UIImageView?

Ответ

Задайте cornerRadius равным половине ширины и включите обрезку:

imageView.clipsToBounds = true
imageView.layer.cornerRadius = imageView.frame.width / 2

Если размер меняется динамически (например, в Auto Layout), установите радиус в viewDidLayoutSubviews() или через расширение с @IBInspectable.


Вопрос

Как сделать скриншот экрана программно?

Ответ

Используйте метод UIGraphicsGetImageFromCurrentImageContext():

UIGraphicsBeginImageContext(view.window!.frame.size)
view.drawHierarchy(in: view.window!.frame, afterScreenUpdates: true)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Этот код создаёт изображение всего окна. Для части экрана передайте нужный UIView.


Вопрос

Как обработать поворот устройства в анимациях?

Ответ

В современных приложениях рекомендуется использовать Auto Layout вместо ручной перестройки. Если требуется кастомная логика, переопределите методы жизненного цикла:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { _ in
// адаптация UI под новый размер
}, completion: nil)
}

Избегайте использования устаревших методов willRotate, так как они не вызываются при использовании trait collections.


Фон, жизненный цикл приложения и системные события

Вопрос

Какие состояния жизненного цикла имеет iOS-приложение?

Ответ

Приложение проходит через пять основных состояний:

  • Not running — приложение не запущено или завершено системой.
  • Inactive — запущено, но не получает события (например, при входящем звонке или свайпе Control Center).
  • Active — запущено на переднем плане и полностью взаимодействует с пользователем.
  • Background — работает в фоне, но может выполнять ограниченные задачи.
  • Suspended — в фоне, но приостановлено системой; не выполняет код и не потребляет ресурсы.

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


Вопрос

Как обрабатывать переход приложения в фон?

Ответ

Реализуйте метод applicationDidEnterBackground(_:) в AppDelegate или используйте уведомления NotificationCenter:

// В AppDelegate.swift
func applicationDidEnterBackground(_ application: UIApplication) {
// Сохранить данные, остановить таймеры, отменить сетевые запросы
}

Или в любом контроллере:

NotificationCenter.default.addObserver(
self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)

Важно завершить все критические операции быстро — система даёт ~5 секунд.


Вопрос

Можно ли выполнять длительные задачи в фоне?

Ответ

Да, но только для определённых типов задач и с явным запросом. Используйте beginBackgroundTask(withName:expirationHandler:):

var backgroundTask: UIBackgroundTaskIdentifier = .invalid

func startLongRunningTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}

// Запустить задачу (например, загрузку)
DispatchQueue.global().async {
// ... долгая операция
self.endBackgroundTask()
}
}

func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}

Система предоставляет до 30 секунд (иногда больше), после чего приложение приостанавливается.


Вопрос

Как обрабатывать завершение приложения?

Ответ

Приложение не получает гарантированного уведомления о завершении. Система может убить его без вызова applicationWillTerminate. Поэтому все данные должны сохраняться немедленно при изменении, а не при выходе.

Тем не менее, метод applicationWillTerminate(_:) может быть вызван при редких сценариях (например, при отладке или если приложение не поддерживает фон):

func applicationWillTerminate(_ application: UIApplication) {
// Финальная очистка (редко используется)
}

Не полагайтесь на него для сохранения данных.


Вопрос

Как реагировать на изменение ориентации устройства?

Ответ

В современных приложениях ориентация управляется через trait collections, а не напрямую через поворот. Переопределите метод:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// Адаптация под новый размер
}

Если нужно ограничить ориентацию, переопределите в контроллере:

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}

И убедитесь, что в Info.plist разрешены нужные ориентации.


Вопрос

Как обрабатывать низкий уровень заряда батареи?

Ответ

Подпишитесь на уведомление UIDevice.batteryLevelDidChangeNotification:

UIDevice.current.isBatteryMonitoringEnabled = true
NotificationCenter.default.addObserver(
self,
selector: #selector(batteryLevelChanged),
name: UIDevice.batteryLevelDidChangeNotification,
object: nil
)

@objc func batteryLevelChanged() {
let level = UIDevice.current.batteryLevel
if level < 0.2 {
// Переключиться в энергосберегающий режим
}
}

Также можно проверять UIDevice.current.batteryState, чтобы отличить зарядку от разрядки.


Вопрос

Как реагировать на потерю интернет-соединения?

Ответ

Используйте NWPathMonitor из фреймворка Network:

import Network

let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
print("Internet available")
} else {
print("No internet")
}
}
monitor.start(queue: DispatchQueue.global())

Это современный и эффективный способ отслеживать состояние сети. Не используйте устаревший Reachability.


Вопрос

Что такое Scene Delegate и зачем он нужен?

Ответ

Начиная с iOS 13, Apple ввела концепцию multiple windows (поддерживается на iPad и macOS). Для управления каждым окном используется SceneDelegate, а AppDelegate отвечает за глобальные события приложения.

Файл SceneDelegate.swift содержит методы вроде:

  • scene(_:willConnectTo:options:) — аналог application(_:didFinishLaunchingWithOptions:)
  • sceneDidEnterBackground(_:) — фон для конкретного окна

Если приложение не поддерживает несколько окон, логика может оставаться в SceneDelegate, но важно понимать разделение ответственности.


Вопрос

Как сохранить состояние приложения при перезапуске?

Ответ

Используйте State Restoration:

  1. В AppDelegate укажите идентификатор восстановления:

    func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
    return true
    }
  2. В каждом контроллере установите restorationIdentifier (в Storyboard или коде).

  3. Реализуйте encodeRestorableState(with:) и decodeRestorableState(with:) для сохранения данных.

При следующем запуске система автоматически воссоздаст иерархию экранов.


Вопрос

Как обрабатывать push-уведомления при запуске из закрытого состояния?

Ответ

При запуске через push-уведомление система передаёт данные в AppDelegate:

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
if let remoteNotification = launchOptions?[.remoteNotification] as? [String: AnyObject] {
handlePush(remoteNotification)
}
return true
}

Также необходимо реализовать делегат UNUserNotificationCenterDelegate для обработки уведомлений в активном состоянии.


Безопасность, шифрование и защита данных

Вопрос

Какие встроенные механизмы безопасности предоставляет iOS для защиты данных приложения?

Ответ

iOS обеспечивает многоуровневую защиту:

  • Песочница (App Sandbox) — каждое приложение изолировано от других и системы.
  • Data Protection API — шифрует файлы на диске с привязкой к состоянию блокировки устройства.
  • Keychain — защищённое хранилище для конфиденциальных данных (токены, пароли).
  • Code Signing — гарантирует, что код не был изменён после подписания Apple.
  • ASLR и DEP — аппаратные механизмы защиты от эксплуатации уязвимостей памяти.

Разработчик должен использовать эти механизмы правильно, чтобы обеспечить безопасность.


Вопрос

Что такое Data Protection и как его включить?

Ответ

Data Protection — это функция iOS, которая автоматически шифрует файлы приложения, когда устройство заблокировано. Уровень защиты задаётся через атрибуты файла.

Пример:

let fileURL = documentsURL.appendingPathComponent("secret.txt")
try "data".write(to: fileURL, atomically: true, encoding: .utf8)

let protectionType = FileProtectionType.completeUntilFirstUserAuthentication
try FileManager.default.setAttributes(
[.protectionKey: protectionType],
ofItemAtPath: fileURL.path
)

Типы защиты:

  • .complete — недоступен, пока устройство заблокировано.
  • .completeUnlessOpen — можно продолжить работу с уже открытым файлом.
  • .completeUntilFirstUserAuthentication — доступен после первого разблокирования (рекомендуется).

Включается автоматически, если включена защита данных в Capabilities проекта.


Вопрос

Как безопасно хранить API-ключи в приложении?

Ответ

Никогда не храните API-ключи в коде или Info.plist — они легко извлекаются из бинарника. Вместо этого:

  1. Используйте серверную прокси-архитектуру: клиент общается с вашим сервером, а сервер — с внешним API.
  2. Если ключ необходим на клиенте, используйте обфускацию (например, разделение ключа на части, шифрование с последующей расшифровкой в рантайме), но помните: это лишь усложняет извлечение, но не делает его невозможным.
  3. Для ключей с ограниченными правами (например, только чтение) можно использовать ограничение по Bundle ID на стороне API.

Идеального решения для клиентских ключей нет — лучшая практика — минимизировать их использование на клиенте.


Вопрос

Как работает шифрование в Keychain?

Ответ

Keychain использует аппаратный модуль безопасности (Secure Enclave на устройствах с Touch ID/Face ID). Каждая запись шифруется уникальным ключом, привязанным к:

  • Идентификатору приложения (Team ID + Bundle ID)
  • Уровню доступа (kSecAttrAccessible)
  • Состоянию блокировки устройства

Даже при рутовании устройства данные Keychain остаются зашифрованными, если не было сделано резервной копии без пароля.


Вопрос

Что означает атрибут kSecAttrAccessible в Keychain?

Ответ

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

  • kSecAttrAccessibleWhenUnlocked — только когда устройство разблокировано.
  • kSecAttrAccessibleAfterFirstUnlock — после первого разблокирования после перезагрузки (подходит для фоновых задач).
  • kSecAttrAccessibleAlwaysустарело, небезопасно.
  • kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly — доступно только если установлен пароль, и данные не синхронизируются.

Рекомендуется использовать kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly для большинства сценариев.


Вопрос

Как предотвратить скриншоты или запись экрана в приложении?

Ответ

Полностью запретить нельзя, но можно скрыть конфиденциальный контент при переходе в фон или активации записи экрана.

Скрываем содержимое при переходе в фон:

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if UIApplication.shared.applicationState == .background {
sensitiveView.isHidden = true
}
}

Обнаруживаем запись экрана:

NotificationCenter.default.addObserver(
self,
selector: #selector(screenRecordingChanged),
name: UIScreen.capturedDidChangeNotification,
object: nil
)

@objc func screenRecordingChanged() {
if UIScreen.main.isCaptured {
// Скрыть чувствительные данные
}
}

Это используется в банковских и медицинских приложениях.


Вопрос

Как защитить сетевой трафик от прослушивания?

Ответ

Используйте HTTPS со строгой проверкой сертификата (Certificate Pinning):

  1. Включите App Transport Security (ATS) в Info.plist (включено по умолчанию).
  2. Реализуйте pinning через URLSessionDelegate:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}

let policy = SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString)
SecTrustSetPolicies(serverTrust, policy)

// Сравнить сертификат с закреплённым отпечатком
if evaluateTrust(serverTrust) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}

Это предотвращает атаки типа «man-in-the-middle».


Вопрос

Что такое Jailbreak detection и стоит ли его использовать?

Ответ

Jailbreak detection — это попытка определить, установлено ли приложение на взломанное устройство. Методы включают проверку существования файлов (/Applications/Cydia.app), записи в защищённые директории, вызовы системных утилит.

Однако:

  • Обходится легко (через tweak-детекторы).
  • Может нарушать правила App Store.
  • Не даёт реальной безопасности — злоумышленник может просто удалить проверку из бинарника.

Лучше полагаться на серверную валидацию и шифрование, а не на обнаружение jailbreak.


Вопрос

Как безопасно удалять данные из приложения?

Ответ

Простое удаление файла не гарантирует стирание данных с диска (особенно на SSD). Для конфиденциальных данных:

  1. Перезапишите содержимое файла нулями перед удалением:

    try data.write(to: fileURL)
    let zeroData = Data(count: data.count)
    try zeroData.write(to: fileURL)
    try FileManager.default.removeItem(at: fileURL)
  2. Используйте Keychain с флагом ThisDeviceOnly, который не синхронизируется и удаляется вместе с приложением.

  3. Для максимальной безопасности — храните данные только в памяти и никогда не записывайте на диск.


Вопрос

Как защитить приложение от реверс-инжиниринга?

Ответ

Полной защиты нет, но можно повысить порог сложности:

  • Включите Strip Debug Symbols в настройках сборки.
  • Используйте обфускацию имён (библиотеки вроде SwiftShield).
  • Избегайте логирования чувствительных данных.
  • Проверяйте целостность кода (через dladdr или хеширование секций), хотя это легко обходится.
  • Переносите критическую логику на сервер.

Помните: если код исполняется на устройстве пользователя, он может быть проанализирован.


Инструменты разработки, CI/CD и автоматизация

Вопрос

Какие инструменты встроены в Xcode для повышения качества кода?

Ответ

Xcode предоставляет несколько встроенных инструментов:

  • Static Analyzer — находит потенциальные ошибки (утечки памяти, nil-разыменование) без запуска программы (Product → Analyze).
  • Fix-its — автоматические исправления синтаксических и стилевых проблем.
  • Refactoring — безопасное переименование, извлечение методов, преобразование в асинхронные функции.
  • Memory Graph Debugger — визуализирует объекты в памяти и обнаруживает retain-циклы.
  • Instruments — набор профилировщиков для анализа производительности, памяти, энергопотребления.

Эти инструменты интегрированы в IDE и не требуют внешних зависимостей.


Вопрос

Как настроить автоматическое форматирование кода в Xcode?

Ответ

Xcode не имеет встроенного автоформаттера, но поддерживает интеграцию с SwiftFormat или swift-format через скрипты сборки или расширения.

Через Build Phase:

  1. Добавьте новый «Run Script» фазу.
  2. Укажите путь к бинарнику SwiftFormat.
  3. Передайте список файлов проекта.

Пример скрипта:

"${SRCROOT}/Tools/swiftformat" --config .swiftformat "${SRCROOT}/MyApp"

Также можно использовать Editor → Structure → Re-Indent для базового форматирования.


Вопрос

Что такое Fastlane и как он используется в iOS-разработке?

Ответ

Fastlane — это набор инструментов с открытым исходным кодом для автоматизации рутинных задач: сборки, тестирования, подписания, публикации в App Store.

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

  • match — синхронизация сертификатов и профилей через Git-репозиторий.
  • gym — сборка IPA.
  • scan — запуск тестов.
  • deliver — загрузка метаданных и приложения в App Store Connect.
  • pilot — управление TestFlight.

Конфигурация хранится в Fastfile:

lane :beta do
build_app(scheme: "MyApp")
upload_to_testflight
end

Запуск: fastlane beta.


Вопрос

Как настроить CI/CD для iOS-приложения?

Ответ

Популярные платформы: GitHub Actions, Bitrise, CircleCI, Jenkins.

Общий workflow:

  1. Клонировать репозиторий.
  2. Установить зависимости (SPM, CocoaPods).
  3. Запустить unit/UI-тесты.
  4. Собрать IPA.
  5. Развернуть на TestFlight или внутренний дистрибутив.

Пример для GitHub Actions:

- name: Run tests
run: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'

- name: Build IPA
run: xcodebuild archive -scheme MyApp -archivePath build/MyApp.xcarchive

Для подписания требуется экспортировать сертификаты и provisioning profiles в секреты CI.


Вопрос

Как управлять сертификатами и provisioning profiles в команде?

Ответ

Рекомендуется использовать Fastlane match:

  1. Создайте приватный Git-репозиторий.
  2. Выполните fastlane match appstore — сертификаты и профили будут созданы и зашифрованы.
  3. Все разработчики запускают fastlane match development, чтобы получить доступ.

Это решает проблему «сертификат работает только у одного разработчика» и упрощает onboarding.


Вопрос

Что такое TestFlight и как им пользоваться?

Ответ

TestFlight — это официальная платформа Apple для бета-тестирования iOS-приложений. Поддерживает до 10 000 внешних тестировщиков и неограниченное число внутренних (до 100 человек из команды App Store Connect).

Этапы:

  1. Соберите IPA с distribution-профилем.
  2. Загрузите через Transporter или Fastlane.
  3. В App Store Connect добавьте сборку в группу тестировщиков.
  4. Тестировщики получают уведомление и устанавливают приложение через TestFlight.

Сборка действует 90 дней.


Вопрос

Как автоматизировать генерацию версий приложения?

Ответ

Версия (CFBundleShortVersionString) и билд (CFBundleVersion) можно обновлять через скрипт в Build Phase:

# Увеличить билд на 1
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${INFOPLIST_FILE}"

Или использовать Fastlane:

increment_build_number

Для семантического версионирования часто связывают номер версии с тегами Git.


Вопрос

Как проверить совместимость кода с новыми версиями Swift?

Ответ

Используйте Migration Assistant в Xcode (Edit → Convert → To Current Swift Syntax).
Также можно запускать сборку с флагом -swift-version в CI, чтобы убедиться, что код совместим с целевой версией.

Рекомендуется указывать версию Swift явно в Package.swift или настройках таргета.


Вопрос

Как отслеживать использование устаревших API?

Ответ

Xcode помечает устаревшие API предупреждениями (@available), если они вызываются без проверки. Чтобы найти все такие места:

  1. Включите строгую проверку: Build Settings → Treat Warnings as Errors.
  2. Используйте условную компиляцию:
    if #available(iOS 15, *) {
    // новый API
    } else {
    // старый API
    }

Также можно использовать сторонние линтеры (например, SwiftLint с правилом discouraged_direct_init).


Вопрос

Как настроить локальный сервер для тестирования API?

Ответ

Используйте URLProtocol для перехвата сетевых запросов и возврата моковых ответов:

class MockURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return true
}

override func startLoading() {
let response = HTTPURLResponse(
statusCode: 200,
url: request.url!
)!
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: mockData)
client?.urlProtocolDidFinishLoading(self)
}
}

Зарегистрируйте в тестах:

URLProtocol.registerClass(MockURLProtocol.self)

Это позволяет тестировать сетевые слои без реального сервера.


Освоение главы0%