Вопросы и ответы с собеседований по Swift
Вопросы и ответы для собеседования Swift-разработчика. Определение и работа со структурами данных, ссылочные типы и типы-значения, протоколы, замыкания, опционалы, generics, различия с Objective C, отложенная инициализация и многое другое.
Какие структуры данных есть в Swift?
В Swift существуют следующие структуры данных:
- enum
- struct
- class
- actor
- protocol
Что такое перечисления (enumerations) в Swift?
Перечисление (enumeration) - это группа связанных значений.
Перечисления позволяют писать безопасный для типов код.
enum Direction {
case North
case East
case South
case West
}
Теперь в своем коде вы можете вызвать, например, Direction.North
, вместо того чтобы использовать мистическую строку "North"
(которая легко может быть неправильно написана и вызвать раздражающие ошибки).
В чем отличие ссылочных типов от типов-значений?
Экземпляры типов-значений (value type) хранятся в стеке, в то время, как экземпляры ссылочных типов (reference type) хранятся в куче.
Также, при присваивании одного экземпляра value type другому экземпляру того же типа происходит копирование из одного экземпляра в другой, в то время как при присваивании одного экземпляра reference type другому экземпляру того же типа происходит копирование ссылки на объект, а не самого объекта.
К value type относятся enum
и struct
.
К reference type относятся class
, actor
, closure
, func
.
Можно ли переопределять структуры и перечисления в Swift?
Нельзя создать подкласс структуры или перечисления, как и переопределить их. Это связано с тем, что структура является типом-значением (value type), и компилятор должен знать ее точный размер во время компиляции, что переопределение делает невозможным.
Что такое Copy On Write в Swift?
Механизм CoW - это стратегия оптимизации памяти, которая используется для избежания неэффективных копирований данных. Что это такое? Когда мы создаем копию какого-либо объекта, структуры или массива, копирование может быть дорогим в плане производительности и затрат памяти. Вместо того, чтобы создавать новую копию объекта, механизм CoW использует "умные" указатели, которые указывают на одну и ту же область памяти до тех пор, пока не происходит изменение данных. Когда данные изменяются, механизм CoW создает новую копию данных, которая может изменяться независимо от оригинала. Как это работает? Механизм CoW используется для работы со структурами и классами в Swift. Когда вы создаете экземпляр структуры или класса, Swift создает копию этого объекта и сохраняет ее в памяти. Внутри структуры или класса могут быть свойства, которые могут изменяться, и когда вы изменяете эти свойства, Swift создает новую копию объекта, которая содержит измененные данные. Процесс работы механизма CoW в Swift можно разделить на три основных этапа: 1. Создание копии объекта. Когда вы создаете копию объекта в Swift, он использует механизм CoW для создания "умного" указателя, который указывает на одну и ту же область памяти, где хранятся данные. 2. Изменение данных. Когда вы изменяете данные в объекте, механизм CoW проверяет, есть ли другие "умные" указатели на ту же область памяти. Если такие указатели есть, механизм создает новую копию объекта, содержащую измененные данные, и перенаправляет указатели на новую область памяти. 3. Удаление объектов. Когда все "умные" указатели на объект становятся недействительными (то есть объект больше не используется в программе), объект должен быть удален из памяти.
Какие типы коллекций есть в Swift?
В Swift есть три типа для работы с коллекциями - Array
, Set
и Dictionary
.
Array
- массив данных. Элементы имеют порядок, доступ к элементам происходит по индексу.
Set
- неупорядоченное множество. В множестве не может быть двух одинаковых элементов. Равенство элементов достигается проверкой hash-значения каждого элемента. В связи с этим элементы множества обязаны реализовать протокол Hashable
. Доступ по индексу невозможен.
Dictionary
- словарь, элементы которого представляют из себя пару (ключ, значение). Ключи словаря должны быть уникальны. Это достигается ровно так же, как и в случае с множеством, через протокол Hashable
. Значения словаря могут быть любым типом, на них не накладываются ограничения. Так же значения не должны быть уникальными. Доступ осуществляется по ключу и возвращает Optional
значение, так как элемента по заданному ключу может не быть. Реализован через hash table.
Какие методы протокола Sequence вы знаете?
1. forEach
- выполняет блок кода для каждого элемента коллекции.
let arr = [1, 2, 3, 4]
arr.forEach { print($0) }
// prints
// 1
// 2
// 3
// 4
2. map
- выполняет блок кода для каждого элемента коллекции, в результате чего образовывается новая коллекция такого же размера, но тип значений коллекции может быть изменен, в зависимости от того, как реализован блок кода, который передается в map
.
let numbers = [1, 2, 3, 4]
let mapped = numbers.map {
Array(repeating: $0, count: $0)
} // mapped = [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
3. flatMap
- выполняет блок кода для каждого элемента коллекции, в результате чего образовывается новая коллекция такого же размера. Отличается от map тем, что, если результирующий элемент коллекции так же является коллекцией, то он будет встроен в результирующую коллекцию. Тип элементов результирующей коллекции так же может измениться, как и при выполнении map
.
let numbers = [1, 2, 3, 4]
let flatMapped = numbers.flatMap {
Array(repeating: $0, count: $0)
}
// flatMapped = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
4. compactMap
- выполняет блок кода для каждого элемента коллекции, в результате чего образовывается новая коллекция. Отличается от map
тем, что размер результирующей коллекции не обязательно равен размеру изначальной. Элементы изначальной коллекции можут быть Optional
и хранить в себе nil
. Такой элемент не попадает в результирующую коллекцию. Тип элементов результирующей коллекции так же может измениться, как и при выполнении map
.
let numbers: [Int?] = [1, nil, 3, 4, nil]
let compactMapped = numbers.compactMap {
if let element = $0 {
Array(repeating: element, count: element)
} else {
return nil
}
}
// compactMapped = [[1], [3, 3, 3], [4, 4, 4, 4]]
5. filter
- выполняет блок кода для каждого элемента коллекции, результатом которого есть значение Bool
. При возврате из блока false
, элемент исключается из результирующей коллекции.
let map = [
"weather": "Good",
"temperature": 27,
"humidity": 34.3
]
let result = map.filter { (key, _) in
key == "temperature"
} // result = ["temperature": 27]
6. reduce
- выполняет блок кода для каждого элемента коллекции, результатом которого становится накопление нового результата. Используется, как правило, для получения единого результата из коллекции.
let arr = [1, 2, 3, 4]
let resut = arr.reduce(0) { partialResult, value in
if partialResult == 0 {
return value
}
return partialResult * 10 + value
} // result = 1234
Что такое протокол в Swift?
protocol
- особый тип данных, экземпляры которого не могут быть созданы. Протоколы описывают будущий функционал, но не реализовывают его. Имплементация протокола кладется на плечи конкретного типа, который собирается его реализовать.
Протокол может быть принят классом, структурой или перечислением для обеспечения фактической реализации этих требований. Любой тип, который удовлетворяет требованиям протокола, реализует данный протокол.
Что такое associated type?
associated type
- механизм параметризации протокола. Используется для обеспечения большей гибкости работы с протоколом в силу ухода от конкретного типа к параметру.
По своей сути generics
для протокола.
Так как протоколы с assotiated type
не являются достаточно конкретными, объявить переменную данного типа или передать напрямую подобный тип как параметр функции невозможно.
Здесь на помощь нам приходят ключевые слова any
и some
. Данные два ключевых слова решают одну и ту же проблему, но немного по-разному.
any
создает коробку вокруг конкретного типа, который удовлетворяет данному протоколу.
some
указывает, что пусть это неизвестно заранее, но на момент компиляции программы конкретный тип переменной будет известен точно.
Что такое Any и AnyObject?
Any
- специальный тип, который является всем сразу и ничем одновременно.
Каждый тип в Swift может быть привиден к типу Any
. Any
используется тогда, когда мы не можем использовать generics, но при этом хотим иметь гибкость передачи аргументов разного типа. Недостаток у такого подхода всего один: так как в итоге тип данных будет Any
, мы должны наверняка знать, какой тип был до этого, чтобы получить возможность восстановить данные после получения.
AnyObject
- специальный тип, такой же, как и Any
, но с ограничением только на reference type. Каждый AnyObject
может быть Any
, но не каждый Any
может быть AnyObject
.
Ключевое слово AnyObject
также используется для ограничения протокола на реализацию только reference type типами.
Что такое замыкания?
Замыкания - это экземпляр функции, которая имеет доступ к определенному контексту и/или захватывает определенные значения и может быть вызвана позже. Замыкания могут захватывать и хранить ссылки на любые константы и переменные из контекста, в котором они объявлены. Эта процедура известна как заключение этих констант и переменных, отсюда и название "замыкание". Swift выполняет всю работу с управлением памятью при захвате за вас. Замыкания принимают одну из трех форм: 1. Глобальные функции являются замыканиями, у которых есть имя и которые не захватывают никакие значения. 2. Вложенные функции являются замыканиями, у которых есть имя и которые могут захватывать значения из включающей их функции. 3. Замыкающие выражения являются безымянными замыканиями, написанные в облегченном синтаксисе, которые могут захватывать значения из их окружающего контекста.
Что такое escaping и nonescaping?
Согласно документации, вы обязаны пометить замыкание, передаваемое в функцию в качестве параметра, ключевым словом @escaping
, если оно будет вызвано после возвращения из функции.
Иными словами, если замыкание (closure) будет вызвано асинхронно внутри (callback) или за пределами (delegate) функции, то вы должны его пометить как @escaping
.
По сути @escaping
позволяет вам отложить выполнение передаваемого в качестве параметра замыкания до нужного вам момента (например, по срабатыванию таймера или по завершению асинхронной операции).
Если вы попробуете присвоить non-escaping
замыкание в свойство класса, структуры или перечисления вы получите compile-time ошибку:
class MainDispatcher {
var work: (() -> Void)? = nil
func async(_ block: () -> Void) {
self.work = block
}
}
Если вы попробуете вызвать non-escaping
замыкание асинхронно (то есть там где ожидается escaping
замыкание), вы также получите compile-time ошибку:
class MainDispatcher {
func async(_ block: () -> Void) {
DispatchQueue.main.async {
block()
}
}
}
По умолчанию замыкание, передаваемое в качестве параметра, является non-escaping
, поэтому там, где вам необходимо вы должны использовать ключевое слово @escaping
.
class MainDispatcher {
var work: (() -> Void)? = nil
func async(_ block: @escaping () -> Void) {
self.work = block
DispatchQueue.main.async {
self.work?()
}
}
func sync(_ block: () -> Void) {
DispatchQueue.main.sync {
block()
}
}
}
Что такое список захвата (capture list)?
Capture list или список захвата - это механизм, использующийся в замыканиях. Благодаря нему замыкание имеет доступ к переменным/константам, объявленными за пределами тела замыкания, а так же к полям типа, внутри которого объявлено замыкание. По умолчанию, все переменные/константы, объявленные внутри функции, в которой объявлено замыкание, попадают в список захвата замыкания. Если данные переменные/константы являются экземплярами ссылочных типов, то они захватываются сильной ссылкой.
self
по умолчанию не захватывается, но любое упоминание self
внутри замыкания автоматически добавляет его в список захвата сильной ссылкой, если self
- экземпляр ссылочного типа.
Список захвата необходим замыканию для того, чтобы была возможность работать с данными, которые объявлены за пределами тела замыкания.
Что такое Optionals (опционалы)?
Optionals (опционалы) — это механизм обработки ситуаций, когда значение переменной может отсутствовать. Значение будет использовано, только если оно есть.
Зачем нужны опционалы, когда есть проверка на nil
?
Во-первых, проверка на равенство/неравенство nil
применима только к nullable
-типам и не применима к примитивным типам, структурам и перечислениям. Для обозначения отсутсвия значения у переменной примитивного типа приходится вводить спецзначения, такие как NSNotFound
.
Соответственно, пользователь этой переменной должен учитывать, что спецзначения возможны. В Swift даже примитивный тип можно использовать в опциональном стиле, т.е явным образом указывать на то, что значения может не быть.
Во-вторых: явная опциональность проверяется на этапе компиляции, что снижает количество ошибок в runtime. Опциональную переменную в Swift нельзя использовать точно так же, как неопциональную (за исключением неявно извлекамых опционалов, подробности в разделе Implicit Unwrapping). Опционал нужно либо принудительно преобразовывать к обычному значению, либо использовать специальные преобразующие идиомы, такие как if let
, guard let
и ??
. Опционалы в Swift реализуют не просто проверку, но целую парадигму опционального типа в теории типов.
В-третьих, опционалы синтаксически более лаконичны, чем проверки на nil
, что особенно хорошо видно на цепочках опциональных вызовов — так называемый Optional Chaining.
Опционал в Swift представляет из себя особый объект-контейнер, который может содержать в себе либо nil
, либо объект конкретного типа, который указывается при объявлении этого контейнера. Эти два состояния обозначаются терминами None
и Some
соответственно. Если при создании опциональной переменной присваемое значение не указывать, то nil
присваивается по умолчанию.
Опционал объявляется посредством комбинации имени типа и лексемы ?
. Таким образом, запись Int?
— это объявление контейнера, экземпляр которого может содержать внутри nil
(состояние None Int
), либо значение типа Int
(состояние Some Int
). Именно поэтому при преобразовании Int?
в Int
используетя термин unwrapping вместо cast, т.е. подчеркивается "контейнерная" суть опционала.
Лексема nil
в Swift обозначает состояние None
, которое можно присвоить любому опционалу. Это логично приводит к невозможности присвоить nil
(состояние None
) переменной, которая не является опционалом.
По факту опционал представляет собой системное перечисление.
Что такое generics и для чего они нужны?
В Swift вы можете использовать generics в классах, структурах и перечислениях.
Generics устраняют проблему дублирования кода. Если у вас есть метод, который принимает параметры одного типа, иногда приходится дублировать код, чтобы работать с параметрами другого типа.
Например, в этом коде вторая функция — это «клон» первой, за исключением того, что у неё параметры string
, а не integer
.
func areIntEqual(_ x: Int, _ y: Int) -> Bool {
return x == y
}
func areStringsEqual(_ x: String, _ y: String) -> Bool {
return x == y
}
areStringsEqual("ray", "ray") // true
areIntEqual(1, 1) // true
Применяя generics, вы совмещаете две функции в одной и одновременно обеспечиваете безопасность типов:
func areTheyEqual<T: Equatable>(_ x: T, _ y: T) -> Bool {
return x == y
}
areTheyEqual("ray", "ray")
areTheyEqual(1, 1)
Так как вы тестируете равенство, вы ограничиваете типы теми, которые отвечают протоколу Equatable
. Этот код обеспечивает требуемый результат и препятствует передаче параметров неподходящего типа.
Что такое typealias в Swift?
Typealias
, как следует из названия, является псевдонимом для существующего типа данных.
Вы можете создать его следующим образом:
typealias Weight = Float
Теперь вы можете использовать Weight
вместо Float
:
let mass1: Weight = 150.0
let mass2: Weight = 220.0
let total: Weight = mass1 + mass2
В чём разница объявления констант в Objective C и Swift?
В Objective-C вы объявляете константу таким образом:
const int number = 0;
А так в Swift:
let number = 0
В чём тут разница?
В Objective-C константа инициализируется во время компиляции значением, которое должно быть известно на этом этапе.
Неизменяемое значение, созданное при помощи let
— это константа, определяемая на этапе выполнения. Вы можете инициализировать ее статическим или динамическим выражением. Поэтому мы можем делать так:
let higherNumber = number + 5
Обратите внимание: такое присвоение возможно сделать лишь однажды.
В чём разница между structure и class?
Классы — ссылочный тип, структуры — тип-значение. Классы имеют дополнительные возможности, которых нет у структур: - Наследование позволяет одному классу наследовать характеристики другого - Приведение типов позволяет проверить и интерпретировать тип экземпляра класса в процессе выполнения - Деинициализаторы позволяют экземпляру класса освободить любые ресурсы, которые он использовал - Подсчет ссылок допускает более чем одну ссылку на экземпляр класса. Для получения дополнительной информации смотрите Наследование, Приведение типов, Деинициализаторы и Автоматический подсчет ссылок.
В каких случаях использовать класс, а в каких — структуру?
В качестве простой шпаргалки: структуры стоит использовать тогда, когда выполняется одно или несколько из следующих условий. - Задача структуры в том, чтобы инкапсулировать несколько относительно простых значений данных; - Можно ожидать, что инкапсулированные значения будут копироваться, а не ссылаться; - Хранящиеся в структуре свойства сами являются типами значений, которые также копируются, а не ссылаются; - Структура не должна наследовать свойства и поведение другого существующего типа. В остальных случаях используйте классы: определите класс, создайте экземпляр для управления и передачи по ссылке.
Какие есть уровни доступа в Swift?
private
- самый закрытый уровень доступа. Поля и методы типа с данным уровнем доступа видны только внутри объявления типа и расширениях, объявленных в том же файле, что и сам тип.
fileprivate
- поля и методы типа с данным уровнем доступа видны снаружи объявления типа, но только в пределах файла, в котором объявлен тип.
internal
- уровень доступа по умолчанию. Поля и методы типа видны снаружи объявления типа в пределах модуля, в котором объявлен тип.
public
- поля и методы типа видны снаружи объявления типа внутри и за пределами модуля, в котором объявлен тип. Однако, за пределами данного модуля тип с данным уровнем доступа не может быть наследован, а поля и методы перегружены.
open
- самый открытый уровень доступа. Тоже самое, что и public
, но наследование и перегрузка не запрещены.
Зачем нужно ключевое слово final?
final
- ключевое слово, которое запрещает дальнейшее наследование типа или перегрузку поля или метода типа.
В чём разница модификаторов static и class?
Чтобы объявить статическое свойство или функцию для типов-значений, используется модификатор static
. Вот пример для структуры:
struct Sun {
static func illuminate() {}
}
А для классов возможно использовать модификаторы static
или class
. Результат один и тот же, но есть отличие: static
делает свойство или функцию статической и неперекрываемой. Использование class
позволит перекрыть свойство или функцию.
Здесь компилятор будет ругаться на попытку перекрыть illuminate()
:
class Star {
class func spin() {}
static func illuminate() {}
}
class Sun : Star {
override class func spin() {
super.spin()
}
// error: class method overrides a 'final' class method
override static func illuminate() {
super.illuminate()
}
}
Какими способами можно развернуть optional?
var x : String? = "Test"
1. Принудительное развёртывание (forced unwrapping) — небезопасно.
let a: String = x!
2. Неявное развертывание при объявлении переменной — небезопасно.
var a = x!
3. Optional binding — безопасно.
if let a = x {
print("x was successfully unwrapped and is = \(a)")
}
4. Optional chaining — безопасно.
let a = x?.count
5. Nil coalescing operator — безопасно.
let a = x ?? ""
6. Оператор guard
— безопасно.
guard let a = x else {
return
}
7. Optional pattern — безопасно.
if case let a? = x {
print(a)
}
Когда не получается избежать неявного развертывания optionals?
Наиболее частые причины для использования implicitly unwrapped optionals:
- когда вы не можете инициализировать свойство, которое не nil
в момент создания. Типичный пример — outlet
у Interface Builder
, который всегда инициализируется после его владельца. В этом особенном случае, если в Interface Builder
всё правильно сконфигурировано — вам гарантировано, что outlet
не-nil
перед его использованием.
- чтобы разрешить проблему цикла сильных ссылок, когда два экземпляра классов ссылаются друг на друга и требуется не-nil
ссылка на другой экземпляр. В этом случае вы помечаете ссылку на одной стороне как unowned
, а на другой стороне используете неявное разворачивание optional.
В чём разница между nil и .none?
Нет никакой разницы, Optional.none
(кратко .none
) и nil
эквивалентны.
Фактически, следующий оператор вернёт true
:
nil == .none
Использование nil
более общепринято и рекомендовано.
Для чего нужен оператор вида «??» ?
Оператор двойного вопросительного знака ??
известен как оператор объединения (слияния) nil
. Он возвращает значение в левой части, если оно не равно nil
. Если левая часть равна nil
, то возвращается значение в правой части.
Его можно использовать как сокращение для проверки того, является ли опциональное значение nil
. Например, вы можете заменить это:
var name: String?
if name != nil {
print(name)
} else {
print("N/A")
}
На это:
print(name ?? "N/A")
Что делает init() в Swift?
Метод init()
используется для инициализации экземпляра.
Инициализация означает подготовку экземпляра (класса, структуры или перечисления) к использованию.
В процессе инициализации вы устанавливаете начальные значения для каждого свойства экземпляра. Вы также можете выполнить некоторые другие подготовительные действия, прежде чем экземпляр будет готов к использованию.
Что такое отложенная инициализация?
Отложенная инициализация — техника задержки создания объекта или выполнения другого процесса до тех пор, пока этот процесс не станет необходимым. Задержку можно использовать только с классами и структурами. Однако стоит понимать, что свойство lazy
не является безопасным, потому что не инициализируется автоматически.
Вам всегда нужно объявлять свойство lazy
как переменную с использованием var
. Свойства констант всегда должны иметь значение до завершения инициализации, поэтому они не могут быть отложенными.
Что такое деинициализаторы в Swift?
Деинициализатор объявляется непосредственно перед освобождением памяти, занимаемой экземпляром класса.
Деинициализатор обозначается ключевым словом deinit
. Он используется, если нужно выполнить какие-либо действия или чистку перед освобождением памяти, занимаемой объектом.
Например, если вы создаете пользовательский класс, чтобы открыть файл и записать в него некоторые данные, вам нужно будет закрыть файл до освобождения памяти, занимаемой экземпляром класса.
Деинициализатор записывается без скобок и не принимает никаких параметров.
deinit {
// выполняем деинициализацию
}
В чем разница между функциями и методами в Swift?
Метод — это функция, связанная с классом, структурой или перечислением. Это относится как к методам экземпляров, так и к методам типов. Функция — объявлена в глобальной области видимости и не относится ни к какому типу. Функции могут быть определены вне классов или внутри классов/структур/перечислений, в то время как методы должны быть определены внутри и быть частью классов/структур/перечислений.
Какой синтаксис у внешних параметров?
Внешний параметр позволяет нам давать имена параметрам функции, чтобы сделать их назначение более понятным.
func power(base a: Int, exponent b: Int) -> Int
Иногда бывает полезно назвать каждый параметр при вызове функции, чтобы указать назначение каждого аргумента, который передается функции.
Если вы хотите, чтобы пользователи вашей функции указывали имена параметров при ее вызове, определите имя внешнего параметра для каждого параметра в дополнение к имени локального параметра.
Как передавать переменные в качестве ссылок?
Переменную можно передавать в качестве ссылки с помощью параметра inout
.
inout
означает, что изменение локальной переменной также изменит передаваемые параметры.
var value: String = “Apple”
func changeString(newValue:inout String) {
newValue = “Samsung”
print(newValue) // Output:Samsung
print(value) // Output:Samsung
}
changeString(newValue:&value)
Зачем нужно ключевое слово mutating?
mutating
— это ключевое слово, используемое в языке Swift для определения методов или функций, которые могут изменять состояние структуры или перечисления. Это означает, что внутренние поля или свойства объекта могут быть изменены во время выполнения метода или функции.
В случае со структурами, изменения могут быть внесены как в свойства инстанса, так и в свойства самой структуры.
В случае с перечислениями, изменения могут быть внесены только в свойства инстанса.
По умолчанию все методы value type типов nonmutating
.
Что такое модуль?
Три возможных определения: 1. Модуль — это отдельная единица в распределении кода; 2. Платформа или приложение, которое создается и распространяется как отдельная единица и может быть импортирована другим модулем; 3. Каждая цель сборки — пакет приложений или фреймворк в Xcode рассматривается как отдельный модуль.
Зачем нужна инструкция defer?
Инструкция defer
используется для выполнения кода непосредственно перед передачей контроля программы за область, в которой она используется.
Инструкция defer
имеет следующий вид:
defer {
выражения
}
Инструкции внутри defer
выполняются независимо от того, каким образом передается управление программой. Это означает, что defer
можно использовать, например, для ручного управления ресурсами, такими как закрытие дескрипторов файлов, а также для выполнения действий, которые должны произойти, даже если возникнет ошибка.
Если в одной и той же области появляются несколько операторов defer
, то порядок их выполнения будет обратным порядку их появления. Выполнение первым последней инструкции defer
данной области видимости означает, что инструкции внутри последней инструкции defer
могут относиться к ресурсам, которые будут очищены другими инструкциями defer
.
func f() {
defer { print("First") }
defer { print("Second") }
defer { print("Third") }
}
f()
// Выведет "Third"
// Выведет "Second"
// Выведет "First"
Операторы в инструкции defer
могут передавать управление программой за пределы инструкции defer
.
Зачем нужна инструкция do?
Инструкция do используется, чтобы ввести новую область и может содержать один или несколько условий catch, содержащих шаблоны, соответствующих определенным условиям обработки ошибок. Переменные и константы, объявленные в области видимости инструкции do, будут доступны только в пределах этой области.
Инструкция do
в Swift похожа на фигурные скобки {}
в C и используется для разделения блока кода, и не снижает производительность во время выполнения.
Инструкция do
выглядит так:
do {
try выражение
инструкции
} catch шаблон 1 {
инструкции
} catch шаблон 2 where условие {
инструкции
}
Как и инструкция switch
, компилятор пытается сделать вывод о том, являются ли условия catch
исчерпывающими. Если такое определение может быть сделано, то ошибка считается обработанной. В противном случае ошибка может распространиться из содержащей ее области, а это значит, что ошибка должна быть обработана с помощью включенного условия catch
или содержащаяся функция должна быть объявлена с throws
.
Для того, чтобы убедиться, что ошибка обрабатывается, используйте условие catch
с шаблоном, который подходит ко всем ошибкам, например шаблон (_
). Если условие catch
не указывает шаблон, то условие catch
подходит и связывает любую ошибку локальной константы с именем error
.
Зачем нужны блоки условной компиляции?
Блок условной компиляции позволяет коду быть условно скомпилированным в зависимости от значения одного или нескольких конфигураций сборки.
Каждый блок условной компиляции начинается с #if
и заканчивается #endif
. Простой блок условной компиляции выглядит так:
#if условие компиляции
выражения
#endif
В отличие от условия инструкции if
, блок условной компиляции оценивается во время компиляции. В результате инструкции компилируются и выполняются только, если блок условной компиляции будет true
во время компиляции.
Что такое extension?
extension
- ключевое слово для дополнения (расширения) уже существующего типа.
Расширить можно любой тип или протокол.
extension
может содержать методы, вычисляемые переменные, статические переменные/константы/методы.
extension
не может объявлять хранимые переменные/константы
extension
не может перегружать переменные/методы, за исключением тех, которые имеют objc
аттрибут или объявлены в objc
коде. Однако, не рекомендуется перегружать подобные переменные/методы в любом случае, так как это может привести к неопределенному поведению системы.
extension
так же используют для предоставления протоколам реализации по умолчанию.
extension SomeType {
// описываем новую функциональность для типа SomeType
}
или для соответствия указанным протоколам:
extension SomeType: SomeProtocol, AnotherProtocol {
// реализация требования протокола тут
}
Чем отличается цикл for от forEach?
Цикл for
имеет все преимущества контроля выполнения и может быть прерван раньше, чем весь цикл будет пройден.
forEach
не может быть прерван и гарантированно пройдет по всем элементам коллекции.
Какая модель работы с памятью в iOS?
iOS использует ARC модель. ARC - Automated Reference Counting - система, которая построена на факте того, что вы не будете работать с объектами напрямую, а будете работать со ссылками на эти объекты. При создании объекта также создается и первая ссылка на данный объект, которая кладется в переменную/константу, которая инициировала создание объекта. При попытке скопировать такой объект система будет создавать новую ссылку на уже существующий объект. Таким образом, когда количество ссылок на объект станет равным нулю, объект удаляется. Через этот механизм реализованы reference type типы Swift и весь ObjC Runtime iOS.
Что такое weak и unowned?
weak
и unowned
- это два ключевых слова для создания слабой ссылки.
Применимо только к экземплярам reference type типов.
Используются для предотвращения reference cycle.
weak
и unowned
отличаются между собой так же, как и операторы ?
и !
соответственно.
weak
не гарантирует наличия значения, в результате чего может использоваться только с optional
.
unowned
предполагает, что значение всегда будет и, по сути, является force unwrapped optional
. Если в процессе выполнения значение unowned
будет nil
, при попытке обратиться к данному значению будет краш.
Можно ли объеденить код на ObjC и Swift в одном проекте?
Да, такая возможность присутствует.
В первую очередь необходимо создать bridging header
файл, который будет инклудить в себя все header
файлы ObjC
кода, которые вы планируете использовать в Swift коде.
Все Swfit типы, которые планируется использовать внутри ObjC
кода, необходимо маркировать @objc
аттрибутом, или наследовать от NSObject
класса.
Можно ли объеденить код на C/C++ и Swift в одном проекте?
Да, такая возможность так же присутствует.
Для начала необходимо выполнить все шаги, которые позволяют использовать ObjC
код в Swift проекте. После чего использовать C/C++ код из ObjC
кода, или переходить на так называемый ObjC++
, смесь ObjC
и C++, так как Swift не может напрямую работать с C/C++ кодом.
Что такое conditional conformance?
conditional conformance
- это механизм языка Swift, который позволяет реализовать протокол generic типом при соблюдении тех или иных условий.
protocol Loggable {
func log()
}
extension Loggable {
func log() {
print(self)
}
}
extension Array: Loggable where Element: Loggable {}
extension Int: Loggable {}
[1, 2, 3, 4].log() // prints [1, 2, 3, 4]
Что такое swizzling?
swizzling
- это механизм ObjC Runtime, который позволяет заменить реализацию одного селектора на другую. Так как определение вызова селектора происходит в процессе исполнения программы, подменить один селектор другим представляется возможным.
Чаще всего swizzling
используется для того, чтобы переписать некоторые селекторы встроенных типов, добавляя туда свой функционал, к примеру, отправку аналитики из UIViewController
.
Что такое Delegate?
Delegate - поведенческий паттерн, который перекладывает часть реализации на другой объект. Делегаты используются в iOS сплошь и рядом в основном для того, чтобы уточнить часть информации, которая зависит от вашего кода и встроить в уже готовую реализацию используемых компонентов.
Примеры: UITableViewDataSource
и UITableViewDelegate
Что такое Observer?
Observer - поведенческий паттерн, который позволяет наладить механизм подписки на события нескольким объектам и реагировать на эти события.
Пример: NotificationCenter
.
Что такое async/await и Task?
async
и await
- это два ключевых слова, которые работают в паре и позволяют выполнять код асинхронно.
async
указывается в объявлении функции или метода и указывает, что данная функция или метод могут быть приостановлены для ожидания выполнения асинхронной операции.
await
пишется перед вызовом любой функции или метода, которые имеют ключевое слово async
в объявлении и являются возможной точкой приостановления для ожидания выполнения асинхронной операции.
Task
- тип, который представлят собой асинхронную задачу. Используется для перехода из синхронной среды в асинхронную. async
функции и методы можно вызвать только внутри Task
или из async
функций или методов.
func doSomeVeryLongStuff() async {
...
}
Task {
await doSomeVeryLongStuff()
}
Что такое actor?
actor
- недавно появившаяся структура данных, которая очень похожа на class
, но имеет ряд отличий.
Во-первых, actor
не может быть унаследован или наследовать другой тип.
Во-вторых, actor
гарантирует безопасность доступа к своим полям и вызова своих методов. Это называется изолированное состояние. Чтение/запись/вызов методов actor
возможно только в асинхронной среде, за исключением тех полей и методов, которые имеют аттрибут nonisolated
.
Что такое reference cycle?
reference cycle - циклическая ссылка, ситуация, при которой один или более объектов указывают друг на друга.
Данная ситуация может произойти по разным причинам, однако, самая частая из них - это неявный захват self
внутри замыкания.
Чтобы избежать reference cycle, необходимо одну из ссылок сделать weak
или unowned
. Какую именно - зависит от реализации программы и бизнес логики.
Что такое счетчик ссылок?
Счетчик ссылок - это механизм всех reference type типов в Swift, благодаря которому reference type типы работают так, как они работают.
Так как при присваивании одному экземпляру reference type типа другого экземпляра этого же типа происходит не копирование реального объекта, а лишь копирование ссылки на него, системе необходимо знать, когда на данный объект никто не будет ссылаться, чтобы мы могли его удалить. Счетчик ссылок как раз и считает ссылки на объект.
Важно уточнить, что ключевые слова weak
и unowned
не влияют на счетчик ссылок. Именно поэтому они всегда являются Optional
и именно поэтому удается решать проблему reference cycle.
Что такое URLSession?
URLSession
- встроенный в iOS тип, с помощью которого приложение может совершать запросы к серверу и получать сообщения в ответ.
Можно создать свою сессию, а можно пользоваться сессией shared
, которая создается системой для каждого приложения.
Какие виды persistency вы знаете в iOS?
"Из коробки" в iOS нам доступны следующие хранилища: UserDefaults
, CoreData
, KeyChain
и запись в файл.
Что такое plist файл?
plist
файл в iOS - это файл, который содержит информацию в виде (ключ, значение), как и Dictionary
.
Используется для различных целей, как правило, содержит набор настроек или информации, специфической для вашего приложения.
Пример: Info.plist
Что такое UserDefaults?
UserDefaults
- одно из локальных хранилищ, которое хранит информацию в виде plist
файла.
Данное хранилище не шифруется, следовательно не предназначено для хранения чувствительной информации. Также данное хранилище не является базой данных и не подходит для хранения больших массивов данных, сложных связей между моделями и не имеет возможности составлять сложные запросы, как СУБД.
Как правило, UserDefaults
используют для хранения настроек приложения или флагов, которые даже при утечки не несут никакой угрозы бизнесу или пользователям приложения.
Расскажите про жизненный цикл приложения
iOS приложение может находится в одном из нескольких состояний:
Not running
- приложение не запущено.
Foreground inactive
- приложение находится на экране, но не является first responder. Такое может произойти, например, когда ваше приложение перекрывает UI телефонного звонка.
Foreground active
- ваше приложение находится на экране и является first responder.
Background
- ваше приложение свернуто и не находится на экране, однако оно все еще работает и получает обновления от системы.
Suspended
- ваше приложение свернуто, не находится на экране и больше не получает обновления от системы.
Что такое Storyboard, Xib, Nib?
Storyboard
, Xib
и Nib
- это, по своей сути, одно и тоже - XML файл, который описывает UI приложения. Формат данного файла не имеет документации и данные файлы не предполагают редактирование за пределами Interface Builder
.
Xib
и Nib
не отличаются вообще ничем, это два названия для одного и тоже термина, на смену Nib
пришел Xib
.
Xib
описывает UI отдельно взятой UIView
или нескольких UIView
.
Storyboard
отличается от Xib
тем, что имеет ряд дополнительных возможностей, так как описывает UI не UIView
, а одного или нескольких UIViewController
. В связи с этим Storyboard
может так же определять взаимодействия и переходы между разными UIViewController
.
Что такое UIKit?
UIKit
- это UI фреймворк, который хранит в себе огромную массу функционала и возможностей для построения UI всего вашего приложения.
Все, что будет отображено на экране и взаимодействовать с пользователем через экран, находится в UIKit
.
Что такое AutoLayout?
AutoLayout
- это система внутри UIKit
, которая отвечает за размещение и размеры элементов на экране приложения.
AutoLayout
работает через constraints
- элементы, которые являются частью уравнения, которое AutoLayout
решает, когда размещает и задает размер ваших UI элементов на экране.
Какое мин. количество constraints надо задать, чтобы определить положение UIView на экране?
Есть несколько вариантов ответа на этот вопрос:
- 2 constraints, одна по вертикали, а вторая по горизонтали, если intrinsicContentSize
вьюхи определен.
- 3 constraints, если intrinsicContentSize
вьюхи определен только по одной оси. Тогда необходим дополнительный constraint по оси, по которой intrinsicContentSize
не определен.
- 4 constraints, по две по вертикали и горизонтали, если intrinsicContentSize
вьюхи не определен.
Назовите основные аттрибуты constraint
constraint
имеет следующие аттрибуты:
- firstItem
- первый якорь элемента, к которому относится данный constraint
.
- secondItem
- второй якорь элемента, к которому относится данный constraint
. Элемент может быть другим или тем же. Якорь может быть nil
.
- constant
- константа constraint
.
- multiplier
- множитель константы или значения якоря элемента.
- relation
- отношение между якорями. Может быть equalTo
, greaterThanOrEqualTo
, lessThanOrEqualTo
`.
- priority
- приоритет constraint
. Находится в диапазоне от 0 до 1000.
Что такое contentHuggingPriority и contentCompressionResistancePriority?
contentHuggingPriority
- это приоритет, который говорит насколько вероятно, что UIView
станет больше своего необходимого размера.
contentCompressionResistancePriority
- это приоритет, который говорит насколько вероятно, что UIView
станет меньше своего необходимого размера.
Данный приоритеты используются вместе с приоритетами constraint для решения неопределенности, при которой две UIView
не могут определить свой размер.
Что такое UIStackView?
UIStackView
- это UIView
, которая не имеет своего графического представления и используется исключительно для расположения своих subViews
по вертикали или по горизонтали.
Преимущества UIStackView
очевидны - меньше ручной настройки UI для достижения тривиальных задач.
Недостатки вытекают отсюда же: если необходимо сделать немного более сложный UI, чем просто расположить элементы один за другим, UIStackView
может создать больше проблем, чем решить.
Расскажите про жизненный цикл UIViewController
У UIViewController
есть следующие методы-обработчики жизненного цикла:
- viewDidLoad
- вызывается всего один раз для каждого UIViewController
в течени жизненного цикла. Сигнализирует о том, что вью была загружена и теперь может быть использована.
- viewWillAppear
- вызывается перед тем, как UIViewController
будет показан на экране. Может быть вызван более одного раза за жизненный цикл.
- viewDidAppear
- вызывается после того, как UIViewController
был показан на экране и анимация его представления завершилась. Может быть вызван более одного раза за жизненный цикл.
- viewWillDisappear
- вызывается перед тем, как UIViewController
будет убран с экрана. Может быть вызван более одного раза за жизненный цикл.
- viewDidDisappear
- вызывается после того, как UIViewController
был убран с экрана и анимация его исчезновения завершилась. Может быть вызван более одного раза за жизненный цикл.
Чем отличаются frame и bounds?
frame
- это прямоугольник, который представляет положение и размер UIView
в системе координат ее superView
.
bounds
- это прямоугольник, который представляет положение и размер UIView
в системе координат этой же UIView
.
origin bounds
всегда находится в координате (0, 0)
.
Что такое UISegue?
UISegue
- это встроенный тип, который отвечает за переход между несколькими UIViewController
, объявленный в Storyboard
.
Чтобы обработать UISegue
, необходимо перегрузить метод prepare
.
Альтернативой использования UISegue
будет создание и презентация UIViewController
вручную в том месте, где необходимо выполнить переход с одного UIViewController
на другой. Как правило UISegue
не используют в силу того, что они всецело опираются на identifier
, который является просто строкой из-за чего работать с ним не очень удобно.
Что такое UINavigationController?
UINavigationController
- это UIViewController
, который является контейнером для других UIViewController
и осуществляет навигацию между ними.
UINavigationController
имеет UINavigationBar
, который показывается в верхней части экрана, а так же имеет стандартные анимации перехода между UIViewController
-ами, переход между которыми происходит в данный момент.
Является одним из самый важных и основных компонентов UI.
Что такое UITabController?
UITabController
- это UIViewController
, который является контейнером для других UIViewController
и осуществляет переход между ними посредстом табов.
UITabController
показывает таб бар в нижней части экрана, где у каждой табы может быть своя иконка и название.
Является одним из основных компонентов UI, однако используется не всегда.
Что такое UITableView?
UITableView
- UIview
, которая представляет собой вертикальный список, каждый элемент которого - это ячейка списка.
UITableView
- это один из наиболее часто используемых компонентов при построении UI приложения и его можно встретить практически в каждом приложении.
Чтобы использовать UITableView
, необходимо реализовать протокол UITableViewDataSource
. В этом случае вы можете отобразить данные, но не сможете с ними взаимодействовать.
Если же необходимо обрабатывать пользовательское взаимодействие с таблицей, необходимо так же реализовать протокол UITableViewDelegate
.
У UITableViewDataSource
есть альтернатива - UITableViewDiffableDataSource
. В отличии от UITableViewDataSource
это не протокол, а конкретный тип, который реализовывает более современный подход к работе с таблицами.
Что такое UICollectionView?
UICollectionView
- UIview
, которая представляет собой коллекцию, состоящую из ячеек. Визуальное представление UICollectionView
зависит от его collectionViewLayout
и может быть абсолютно любым. Самый частый случай - это вертикальная или горизонтальная сетка элементов. Так же как и UITableView
является одним из наиболее часто используюемых компонентов при построении UI приложения.
Чтобы использовать UICollectionView
, необходимо реализовать протокол UICollectionViewDataSource
. В этом случае вы можете отобразить данные, но не сможете с ними взаимодействовать.
Если же необходимо обрабатывать пользовательское взаимодействие с коллекцией, необходимо так же реализовать протокол UICollectionViewDelegate
.
У UICollectionViewDataSource
есть альтернатива - UICollectionViewDiffableDataSource
. В отличии от UICollectionViewDataSource
это не протокол, а конкретный тип, который реализовывает более современный подход к работе с коллекциями.
Так же, если вы хотите использовать collectionViewLayout
, отличный от UICollectionViewFlowLayout
, его также необходимо отдельно создать, настроить и указать коллекции.
В чем разница между UICollectionView и UITableView?
UITableView
способна отображать свои элементы исключительно как вертикальный список, в то время, как элементы коллекции могут отображаться в абсолютно произвольной форме.
Современные нововведения в UICollectionView
позволяют полностью перестать использовать UITableView
.
Что такое GestureRecognizer?
GestureRecognizer
- это встроенный тип, позволяющий обрабатывать различные жесты пользователя. Они бывают следуюших типов:
UITapGestureRecognizer
- обрабатывает короткое нажатие.
UIPinchGestureRecognizer
- обрабатывает двухпальцевый жест увеличения и уменьшения.
UIRotationGestureRecognizer
- обрабатывает двухпальцевый жест поворота.
UISwipeGestureRecognizer
- обрабатывает быстрый смах по экрану.
UIPanGestureRecognizer
- обрабатывает жест перемещения пальца по экрану.
UIScreenEdgePanGestureRecognizer
- обрабатывает жест перемещения пальца от края экрана.
UILongPressGestureRecognizer
- обрабатывает жест длинного нажатия на экран.
UIHoverGestureRecognizer
- обрабатывает жест наведения курсора на элемент UI. Доступен только на iPadOS.
Что такое LaunchScreen.storyboard?
LaunchScreen.storyboard
- это специальный storyboard
, который показывается в момент запуска приложения.
Содержимое данного storyboard
нельзя динамически настраивать.
Что такое size class?
Size class
- это параметр, указывающий на то, в каком окружении в данный момент выполняется наше приложение.
Есть два значения size class
- по-вертикали и по-горизонтали. Комбинация этих значений может указать нам на диапазон устройств и состояний устройств, в которых мы сейчас исполняемся. Используется для построения различного UI в зависимости от доступного пространства на экране.
Size class
может быть одним из следующих значений:
- compact
- regular
- unspecified
Как построить UI в коде?
Для того, чтобы построить UI в коде, необходимо всю работу Interface Builder
-а взять на себя. А именно, создать экземпляры необходимых UIView
, настроить их нужным образом, добавить во view hierarchy и задать между ними constraints
.
Преимущества данного подхода в том, что, во-первых, вы больше не связываетесь с Interface Builder
. Больше не может случится ситуация, при которой в файле IB
случилась непоправимая ошибка, в результате чего Xcode больше не может отрендерить UI в принципе и вам надо вручную искать ошибку в сгенерированном недокументированном файле.
Во-вторых, мерж конфликты решать в коде значительно проще, чем в IB
файлах.
Недостатки вытекают из достоинств: слишком много кода, который просто настраивает UI, наглядно не видно, что именно и как настроено, работать с size class
становится немного сложнее.
В чем разница между CALayer и UIView?
CALayer
является компонентом CoreGraphics
- низкоуровнего API, который непосредственно занимается рендером и анимациями.
CALyaer
является наследником CGLayer
.
UIView
является контейнером для CALayer
и у каждой UIView
есть как минимум один CALayer
, как у каждого UIViewController
есть как минимум один UIView
.
Как можно реализовать свою анимацию перехода между двумя экранами?
Для этого необходимо реализовать протокол UIViewControllerAnimatedTransitioning
, затем задать значение custom
в поле modalPresentationStyle
.
Значением полю transitioningDelegate
присвоить объект, который реализовал UIViewControllerAnimatedTransitioning
.
Все данные поля необходимо присваивать объекту UIViewController
, с которого необходимо реализовать кастомный переход.
Что такое UISplitViewController?
UISplitViewController
- это UIViewController
, который является контейнером для других UIViewController
.
UISplitViewController
располагает свои viewControllers
друг рядом с другом по горизонтали. Первая колонка по умолчанию будет значительно уже второй. Данное расположение доступно только для девайсов, которые имеют regular
size class
по горизонтали. При значении compact
size class
по горизонтали UISplitViewController
превращается в обычный UINavigationController
.
С недавних пор UISplitViewController
был обновлен. Ему полностью переписали API, добавили возможность сделать три колонки, а так же сделать полноценно отдельный UIViewController
для девайсов со значением compact
size class
по горизонтали.
В чем разница между layoutIfNeeded(), setNeedsLayout() и layoutSubviews()?
layoutIfNeeded()
- метод UIView
, который инициирует полный перерасчет всего UI данной UIView
и всех ее subViews
, если текущий UI не актуален.
setNeedsLayout()
- метод UIView
, указывающий UIView
, что текущее отображение UI данной UIview
более не актуально и должно быть пересчитано.
layoutSubviews()
- метод UIView
, который инициирует полный перерасчет всего UI данной UIView
и всех ее subViews
. В отличии от layoutIfNeeded()
не проверяет текущий UI на актуальность.
Что такое UIResponder?
UIResponder
- тип UIKit
, который отвечает за обработку всех событий пользователя.
Обработка событий UIResponder
происходит по цепочке ответственности. Если данный UIResponder
не может обработать то или иное событие, он пробрасывает событие далее своим дочерним UIResponder
.
Так событие опускается до тех пор, пока не будет обработано, или, пока не закончатся UIResponder
-ы.
Какие UI операции можно делать не на главном потоке?
Вопрос с подвохом. Если мы говорим об операциях, которые непосредственно взаимодействуют с UI нашего приложения, который находится на экране или во вью стеке, но все подобные операции обязаны выполняться только на главном потоке. Без исключений.
Если же мы говорим о UI операциях в целом, то все операции, которые не взаимодействуют с UI во вью стеке, можно выполнять где-угодно. Другими словами, получить данные картинки и создать UIImage
можно на любом потоке, но присвоить UIImageView
поле image можно только на главном потоке, если данная UIImageView
находится во вью стеке.
Что такое UIScene?
UIScene
- тип UIKit
, который появился в iOS 13.
UIScene
отвечает за хранение UIWindow
приложения.
UIScene
имеет смысл только на iPadOS
. Данный тип позволяет осуществлять запуск более одного экземпляра приложения одновременно. Также, благодаря UIScene
на iPadOS
работает мультиоконный режим, при котором несколько приложений могут находится на экране одновременно.
Сколько LaunchScreen сторибордов может быть в приложении?
По умолчанию, при создании любого iOS приложения, Xcode сгенерирует один LaunchScreen.storyboard
, который будет использовать при любом запуске приложения. Однако, вы можете создать и другие подобные storyboard
-ы.
При реализации URL схем и Push notification, вы можете указать какой конкретно LaunchScreen.storyboard
вы хотели бы использовать, если при обработке схемы или уведомления приложение будет запускаться с нуля.
Что такое CADisplayLink?
CADisplayLink
- тип фреймворка UIKit
, который отвечает за синхронизацию с тактом рендера.
CADisplayLink
используется для того, чтобы выполнять действия ровно каждый кадр.
Что такое SwiftUI?
SwiftUI - новый фреймворк для UI, который не столько строит и рендерит сам UI, сколько описывает его и опирается на реальный функционал других API, в зависимости от того, на какой платформе будет исполняться код, написанный с помощью SwiftUI. Данный фреймворк все еще довольно молод и не позволяет воссоздать абсолютно все, что можно сделать на UIKit/AppKit, однако SwiftUI позволяет внедрять UIKit/AppKit код в SwiftUI код и наоборот.
Что такое View
View
- основной протокол фреймворка SwiftUI.
Все, что отображается на экране, будь то целый экран, или его элемент, должны реализовать данный протокол.
Что такое ViewModifier?
ViewModifier
- второй по значимости протокол в SwiftUI.
С его помощью мы можем модифицировать уже созданную View
, меняя текущие параметры или добавляя новые.
Что такое CoreData?
Core Data — фреймворк от компании Apple, встроенный в операционную систему iOS, MacOS, который позволяет разработчику взаимодействовать с базой данных. Был представлен компанией Apple c анонсом Mac OS X 10.4 Tiger и iOS с iPhone SDK 3.0. Позволяет данным быть организованными в Сущность-Атрибут-Значение (EAV). Управление данными может быть осуществлено с помощью манипуляций сущностей и их взаимосвязей.