Здесь собраны на мой взгляд самые распространненные вопросы на собеседовании iOS разработчиков разного уровня. К большинству из них даны ответы и примеры кода, где это необходимо. Ниже представлена легенда, по которой я буду маркировать вопросы разного уровня. Данная маркировка весьма условна и лишь является моим видением того, какую информацию человек должен знать, если он претендует на тот или иной уровнеь. Каждый следующий уровень включает в себя все вопросы предыдущего.
🟢 - Junior
уровень
🟡 - Middle
уровень
🔴 - Senior
уровень
let example = "Hello, Example!"
Мои комментарии
Три основных принципа OOP
- Инкапсуляция, Наследование, Полиморфизм.
Инкапсуляция - механизм изолирования реализации (переменных и функций) под область видимости одного типа.
Наследование - механизм, при котором один тип может получить все свойства другого типа, который является его предком.
Полиморфизм - механим представления одного типа через другой тип, при условии, что оба типа являются родственниками.
Наверное, самый стандартный и одновременно всех задолбавший вопрос на собеседовании, без которого, наверное, планета уже давно сошла бы с оси.
POP
- Protocol Oriented Programming, подход, при котором структура программы реализуется через протоколы (интерфейсы) и их реализацию. POP
отличается от OOP
тем, что при использовании POP
, вы создаете протоколы, которые затем реализуют ваши типы, а не создаете типы, которые потом необходимо наследовать.
Функция отличается от метода областью видимости. Функцию можно вызвать глобально и она не привязана ни к какому типу, в то время, как метод можно вызвать только у экземпляра типа. При этом метод имеет доступ ко всем полям и другим методам данного экзепляра.
Рекурсия
- это механизм, который позволяет функции вызвать саму себя. При неосторожном использовании рекурсии, программа может провалиться в бесконечный цикл вызовов, что в итоге приведет к крашу в тот момент, когда переполнится стэк вызова функций. Для того, чтобы избежать подобного сценария у рекурсии должно быть так называемое дно
- точка выхода из рекурсии, где функция больше не вызывает саму себя.
Big-O notation
- показатель сложности алгоритма. Данный показатель может использоваться для оценки сложности алгоритма по скорости или по памяти.
Этот вопрос очень любят, рекомендую изучить данную тему.
KVO
- Key-Value Observing - механизм, при котором мы можем отслеживать изменения свойства экземпляра, получая доступ к данному свойству по ключу.
KVM
- Key-Value Modification - механизм, который позволяет нам модифицировать свойство экземпляра, получая доступ к данному свойству по ключу.
Данный вопрос встречается довольно редко, поэтому многих может застать врасплох.
Здесь я рекомендую назвать только те, которые вы реально знаете, так как их могут попросить реализовать на месте. Если вы знаете и помните наизусть много алгоритмов, можете назвать пару-тройку из них.
Лично я крайне негативно отношусь к данному вопросу, так как все мы прекрасно понимаем, что в реальности в 99% случаях будет использованы
sort()
иfilter()
из стандартной библиотеки.
Реактивное программирование
- программирование, при котором общение между объектами программы происходит посредством налаживанием каналов связи между объектами. Реактивный подход очень активно использует функциональное программирование - тип программирования, при котором значения не хранятся в переменных, а высчитываются, пропускаясь через цепочку функций.
Если у вас есть опыт реакта, рекомендую четко указывать, каким фреймворком для этого пользовались. К примеру,
RxSwift
иCombine
имеют очень много различий и умение пользоваться одним не так легко проэцируется на другой.
Future
или Promise
- два названия одного и того же подхода, одного из способов реализации рективного программирования. Это прокси (контейнер), в котором должно быть значение в каком-то времени в будущем. Изначально данный контейнер может быть пуст и при попытки получить его значение, выполнение программы блокируется до тех пор, пока значение в контейнере не появится.
Publisher
и Subscriber
- это еще один подход, с помощью которого возможна реализация реактивного программирования.
Publisher
- объект, который выдает значения в течении некоторого или неограниченного времени.
Subscriber
- объект, который подписывается на выдачу значений publisher
'a и обрабатывает получаемые значения от него.
Слабосвязанный код
- это код, который написан такм образом, при котором один фрагмент кода может быть заменен на другой фрагмент кода, не требуя при этом изменений в других фрагментах кода. Как правило это достигается при помощи протоколов. Преимущество перед сильносвязанным кодом в том, что мы можем с куда меньшими затратами сил и времени менять часть программы.
protocol ColorProvider {
var color: UIColor { get }
}
struct LCDColorProvider: ColorProvider {
var color: UIColor {
UIColor(red: 0.3, green: 0.24, blue: 0.71, alpha: 1)
}
}
struct P3ColorProvider: ColorProvider {
var color: UIColor {
UIColor(displayP3Red: 0.32, green: 0.264, blue: 0.7142, alpha: 1)
}
}
struct ColorSettings {
var colorProvider: ColorProvider
}
let lcdSettings = ColorSettings(colorProvider: LCDColorProvider())
let p3Settings = ColorSettings(colorProvider: P3ColorProvider())
Вопрос достаточно обширный и зависит от методов и предпочтений каждого. Можете перечислить все, что вы используете, желательно максимально детально.
Аналогично предыдущему вопросу, просто перечисляем все способы, которые реально использовали на практике. Существует несколько самых распространенных способов данной оптимизации, они довольно легко ищутся в интернете.
В Swift существуют следующие структуры данных:
enum
struct
class
actor
protocol
Описание каждой структуры с легкостью находятся в книге The Swift Programming Language.
К value type
относятся enum
и struct
.
К reference type
относятся class
, actor
. Так же к reference type
относятся closure
и func
, хоть они и не являются структурами данных.
Экземпляры value type
хранятся в стеке (stack), в то время, как reference type
хранятся в куче (heap).
Так же, при присваивании одного экземпляра value type
другому экземпляру того же типа, происходит копирование из одного экземпляра в другой, в то время, как при присваивании одного экземпляра reference type
другому экземпляру того же типа, происходит копирование ссылки на объект, а не самого объекта.
closure
или замыкания - это анонимные функции, которые не могут принадлежать типу, но могут храниться в переменной. Так же closure
можно определить в том месте, где ожидается параметр-функция. По своей сути closure
и func
- это одно и тоже.
escaping
и nonescaping
- ключевые слова, применимые только к параметрам функции, типом которых является функция. Являются механизмом оптимизации выполнения кода.
escaping
- сигнализирует компилятору, что функция-параметр может быть вызвана после того, как выполнение тела вызывающей ее функции закончится.
nonescaping
, соотвественно, гарантирует, что функция-параметр не будет вызвана после того, как тело вызывающей ее функции завершится.
По умолчанию все параметры-функции - nonescaping
, за исключением, если они не являются optional
.
Capture list или список захвата - это механизм, использующийся в замыканиях. Благодаря ему замыкание имеет доступ к переменным/константам, объявленными за пределами тела замыкания, а так же к полям типа, внутри которого объявлено замыкание. По умолчанию, все переменные/константы, объявленные внутри функции, в которой объявлено замыкание, попадают в список захвата замыкания. Если данные переменные/константы являются экземплярами reference type
типов, то они захватываются сильной ссылкой.
self
по умолчанию не захватывается, но любое упоминание self внутри замыкания автоматически добавляет его в список захвата сильной ссылкой, если self
- экземпляр reference type
.
Список захвата необходим замыканию для того, чтобы была возможность работать с данными, которые объявлены за пределами тела замыкания.
closure
- это reference type
. Если бы closure
была бы value type
, то при передаче ее в функцию, она бы копировалась, что привело бы к копированию всех объектов, которые попадают в ее список захвата и потерю контекста, где closure
была определена.
func
- это так же reference type
. По своей сути closure
и func
- это одно и тоже, так что и "под капотом" они реализованы одинаково.
CoW
- это механизм, при котором при присваивании одного экземпляра другому, копирование объекта не происходит до тех пор, пока один из экземпляров не будет модифицирован. Данный механизм используется во всех value type
типах в Swift.
mutating
- это ключевое слово в Swift, которое позволяет методу, указанному как mutating
, менять self
в value type
типах. По умолчанию все методы value type
типов nonmutating
.
10. 🟢 В чем отличие между var
и let
? Есть ли разница между value type
и reference type
с точки зрения var
и let
?
var
- ключевое слово для объявления переменной.
let
- ключевое слово для объявления константы.
Переменные value type
и reference type
ведут себя одинаково.
Константе value type
не может быть присвоен другой экземпляр данного типа, а так же все ее внутренние поля не могут быть изменены.
Константе reference type
не может быть присвоен другой экземпляр данного типа, но ее внутренние неконстантные поля могут быть изменены.
private
- самый закрытый уровень доступа. Поля и методы типа с данным уровнем доступа видны только внутри объявления типа и расширениях, объявленных в том же файле, что и сам тип.
fileprivate
- поля и методы типа с данным уровнем доступа видны снаружи объявления типа, но только в пределах файла, в котором объявлен тип.
internal
- уровень доступа по умолчанию. Поля и методы типа видны снаружи объявления типа в пределах модуля, в котором объявлен тип.
public
- поля и методы типа видны снаружи объявления типа внутри и за пределами модуля, в котором объявлен тип. Однако, за пределами данного модуля тип с данным уровнем доступа не может быть наследован, а поля и методы перегружены.
open
- самый открытый уровень доступа. Тоже самое, что и public
, но наследование и перегрузка не запрещены.
public
типы не могут наследоваться, а public
поля и методы не могут перегружаться за пределами модуля, в котором объявлен тип.
open
типы могут наследоваться, а open
поля и методы могут перегружаться за пределами модуля, в котором объявлен тип.
final
- ключевое слово, которое запрещает дальнейшее наследование типа или перегрузку поля или метода типа.
enum
в Swift может иметь rawValue
отличный от Int
, в отличии от многих языков программирования. Так же, каждое значение перечисления может иметь вложенные значения любого типа в любом количестве, включая данное перечисление. Если вложенное значение является данным перечислением, перед объявлением данного значения ставится ключевое слово indirect
.
Optional
- специальное перечисление в Swift, которое позволяет показать, что значение отсутствует, что обозначается ключевым словом nil
.
if let
- это конструкция безопасного разворачивания Optional
. Код внутри блока if let
будет выполнен только в том случае, если проверяемый Optional
не равен nil
.
var myNumber: Int? = 12
if let number = myNumber {
print("My number is \(number)") // prints Mu number is 12
}
guard
- это ключевое слово контроля выполнения кода, которое проверяет условие на истинность. У guard
есть только блок else
, который выполнится, если условие не действительно. else
блок guard
обязан прервать выполнение блока кода, в котором объявлен guard
.
func increment(number: Int) -> Int {
guard number >= 10 else {
return number
}
return number + 1
}
let incrementedNumber = increment(number: 4) // result is 5
let notIncrementedNumber = increment(number: 13) // result is 13
Обе данных конструкции безопасно разворачивают Optional
и выполняют блок кода только в том случае, если значение Optional
не равно nil
. Единственное различие в том, что else
блок guard
обязан выйти из блока кода, где был объявлен guard
.
Оператор ?
- это оператор опционального связывания при доступе к значению Optional
.
struct Person {
var name: String
var lastName: String
}
var me: Person? = Person(name: "Karl", lastName: "Brown")
me?.name = "Mark"
Оператор !
- это оператор небезопасного разворачивания Optional
. Должен использоваться только тогда, когда вы на 100% уверенны, что значение Optional
не равно nil
.
var number: Int? = 10
var noNumber: Int?
print(number!) // ok, prints 10
print(noNumber!) // crash, unexpectedly found nil while unwrapping an optional value
Оператор ??
- это оператор безопасного разворачивания Optional
и использования его значения или предоставления значения по умолчанию, если значение Optional
равно nil
.
var number: Int? = 10
var noNumber: Int?
print(number ?? 12) // prints 10
print(noNumber ?? 12) // prints 12
extension
или расширение - дополнение уже существующего типа. Расширить можно любой тип или протокол. extension
может содержать методы, вычисляемые переменные, статические переменные/константы/методы. extension
не может объявлять хранимые переменные/константы. extension
не может перегружать переменные/методы, за исключением тех, которые имеют objc
аттрибут или объявлены в objc
коде. Однако, не рекомендуется перегружать подобные переменные/методы в любом случае, так как это может привести к неопределенному поведению системы.
extension
так же используют для предоставления протоколам реализации по умолчанию.
protocol
- особый тип данных, экземпляры которого не могут быть созданы. Протоколы описывают будущий функционал, но не реализовывают его. Имплементация протокола кладется на плечи конкретного типа, который собирается его реализовать. Протоколы используются для обеспечения менее связанного кода и предоставление гибкости при разработке, так как можно не полагаться на конкретные типы, а на протоколы. При таком подходе любой тип, который реализует протокол, может быть использован в данном месте программы.
Any
- специальный тип, который является всем сразу и ничем одновременно. Каждый тип в Swift может быть привиден к типу Any
. Any
используется тогда, когда мы не можем использовать generics
, но при этом хотим иметь гибкость передачи аргументов разного типа. Недостаток у такого подхода всего один: так как в итоге тип данных будет Any
, мы должны наверняка знать, какой тип был до этого, чтобы получить возможность восстановить данные после получения.
AnyObject
- специальный тип, такой же, как и Any
, но с ограничением только на reference type
. Каждый AnyObject
может быть Any
, но не каждый Any
может быть AnyObject
. Ключевое слово AnyObject
так же используется для ограничения протокола на реализацию только reference type
типами.
В Swift есть три типа для работы с коллекциями - Array
, Set
и Dictionary
.
Array
- массив данных. Элементы имеют порядок, доступ к элементам происходит по индексу.
Set
- неупорядоченное множество. В множестве не может быть двух одинаковых элементов. Равенство элементов достигается проверкой hash
значения каждого элемента. В связи с этим элементы множества обязаны реализовать протокол Hashable
. Доступ по индексу невозможен.
Dictionary
- словарь, элементы которого представляют из себя пару (ключ, значение). Ключи словаря должны быть уникальны. Это достигается ровно так же, как и в случае с множеством, через протокол Hashable
. Значения словаря могут быть любым типом, на них не накладываются ограничения. Так же значения не должны быть уникальными. Доступ осуществляется по ключу и возвращает Optional
значение, так как элемента по заданному ключу может не быть. Реализован через hash table
.
Скорее всего здесь вас попросят рассказать про
hash table
поподробнее. Данный вопрос я освещать не буду, так как он чисто документальный и скорее всего никогда в жизни вам не пригодится на практике.
Если в Set
попытаться добавить два объекта с одинаковым значением hash
, то при добавлении первого элемента, он будет успешно добавлен в множество. При попытке добавить второй элемент в множество, элемент добавлен не будет.
25. 🟢 Что такое forEach
/map
/flatMap
/compactMap
/filter
/reduce
? Какие еще примеры функционального программирования вы знаете в Swift?
forEach
, map
, flatMap
, compactMap
, filter
и reduce
- функции протокола Sequence
в Swift, которые дают возможность работать с коллекциями, используя функциональный подход.
В данном случае:
forEach
- выполняет блок кода для каждого элемента коллекции.
let arr = [1, 2, 3, 4]
arr.forEach { print($0) }
// prints
// 1
// 2
// 3
// 4
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]]
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]
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]]
filter
- выполняет блок кода для каждого элемента коллекции, результатом которого есть значение Bool
. При возврате из блока false
, элемент исключается из результирующей коллекции.
let map = [
"weather": "Good",
"temperature": 27,
"humidity": 34.3
]
let result = map.filter { (key, _) in
key == "temperature"
} // result = ["temperature": 27]
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
Это далеко не все подобный функции коллекций, но про эти спрашивают чаще всего.
compactMap
- функция, которая формирует новую коллекцию из текущей, исключая все элементы, значением которых является nil
.
flatMap
- функция, формирует новую коллекцию, "схлопывая" накопление вложенные коллекциии или Optional
до одной коллекции или Optional
.
weak
и unowned
- это два ключевых слова для создания слабой ссылки. Применимо только к экземплярам reference type
типов. Используются для предотвращения reference cycle
. weak
и unowned
отличаются между собой так же, как и операторы ?
и !
соответственно. weak
не гарантирует наличия значения, в результате чего может использоваться только с optional
. unowned
предполагает, что значение всегда будет и, по сути, является force unwrapped optional
. Если в процессе выполнения значение unowned
будет nil
, при попытке обратиться к данному значению будет краш.
Здесь можно много говорить, но лучше один раз показать, чем сто раз рассказать, так что просто воспроизводим enum
из стандартной библиотеки. Благо он небольшой.
enum Optional<Wrapped> {
case some(Wrapped)
case none
}
Да, все так просто
lazy var
- это переменная, инициализация которой не происходит во время инициализации экземпляра. Вместо этого инициализация lazy var
происходит при первом обращении к этой переменной. Используется для оптимизации ресурсов, когда тяжелые или длительные операции, которые не всегда нужны, можно отложить до реальной необходимости.
У lazy var
есть интересный побочный эффект: так как инициализация данной переменной происходит после инициализации экземпляра, при инициализации lazy var
self
уже существует и может быть использован.
genrics
- аналог шаблонов из C++, механизм парметризации типов, способ уйти от конкретного типа к передачи его как параметра и обобщения функционала для разных типов. Идеальный пример - Array
. Мы можем создать массив любых данных, но каждый такой массив будет работать одинаково, вне зависимости от типа данных, которые он хранит.
associated type
- механизм параметризации протокола. Используется для обеспечения большей гибкости работы с протоколом в силу ухода от конкретного типа к параметру. По своей сути generics
для протокола.
Так как протоколы с assotiated type
не являются достаточно конкретными, объявить переменную данного типа или передать напрямик подобный тип как параметр функции невозможно.
Здесь на помощь нам приходят ключевые слова any
и some
. Данные два ключевых слова решают одну и ту же проблему, но немного по-разному.
any
- создает коробку вокруг конкретного типа, который удовлетворяет данному протоколу.
some
- указывает, что пусть это не известно заранее, но на момент компиляции программы конкретный тип переменной будет известен точно.
Цикл for
имеет все преимущества контроля выполнения и может быть прерван раньше, чем весь цикл будет пройден.
forEach
не может быть прерван и гарантированно пройдет по всем элементам коллекции.
Да, такая возможность присутствует. В первую очередь необходимо создать bridging header файл, который будет инклудить в себя все header файлы ObjC кода, которые вы планируете использовать в Swift коде. Все Swfit типы, которые планируется использовать внутри ObjC кода, необходимо маркировать @objc
аттрибутом, или наследовать от NSObject класса.
Да, такая возможность так же присутствует. Для начала необходимо выполнить все шаги, которые позволяют использовать ObjC код в Swift проекте. После чего использовать C/C++ код из ObjC кода, или переходить на так называемый ObjC++, смесь ObjC и C++, так как Swift не может напрямую работать с C/C++ кодом.
При вызове функции или метода в Swift функция, которую мы собираемся вызвать заранее определена и точно существует. Это может быть глобальная фукнция, метод экземпляра, или типа, функция может быть перегружена, но она точно есть и определена. Swift так работает в силу того, что является жестко типизированным языком.
ObjC не имеет фукнций или методов, зато имеет сообщения, или селекторы, при вызове которых ObjC runtime пытается найти вызываемый селектор у объекта, у которого вы пытаетесь его вызвать. Если селектор будет найден, он будет вызван. Если нет - ничего не произойдет.
Главное различие данных подходов в том, что вызов функции в Swift определяется на этапе компиляции, в то время как вызов селектора в ObjC - на этапе выполнения. Это позволяет в ObjC определять селекторы, которые вызываются после того, как программа уже написана, просто динамически подгружая их, к примеру, через библиоотеку.
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]
async
и await
- это два ключевых слова, которые работают в паре и позволяют выполнять код асинхронно.
async
- указывается в объявлении функции или метода и указывает, что данная функция или метод могут быть приостановлены для ожидания выполнения асинхронной операции.
await
- пишется перед вызовом любой функции или метода, которые имеют ключевое слово async
в объявлении и являются возможной точкой приостановления для ожидания выполнения асинхронной операции.
Task
- тип, который представлят собой асинхронную задачу. Используется для перехода из синхронной среды в асинхронную. async
функции и методы можно вызвать только внутри Task
или из async
функций или методов.
actor
- недавно появившаяся структура данных, которая очень похожа на class
, но имеет ряд отличий. Во-первых, actor
не может быть унаследован или наследовать другой тип. Во-вторых, actor
гарантирует безопасность доступа к своим полям и вызова своих методов. Это называется изолированное состояние. Чтение/запись/вызов методов actor
возможно только в асинхронной среде, за исключением тех полей и методов, которые имеют аттрибут nonisolated
.
func doSomeVeryLongStuff() async {
...
}
Task {
await doSomeVeryLongStuff()
}
swizzling - это механизм ObjC Runtime, который позволяет заменить реализацию одного селектора на другую. Так как определение вызова селектора происходит в процессе исполнения программы, подменить один селектор другим представляется возможным.
Чаще всего swizzling используется для того, чтобы переписать некоторые селекторы встроенных типов, добавляя туда свой функционал, к примеру, отправку аналитики из UIViewController.
Несмотря на то, что swizzling не нарушает никакие гайдлайны, использование swizzling необходимо тщательно обдумать, так как вычислить имеет ли он место быть или нет, не зная заранее ответ на этот вопрос, может быть не просто.
Лично я считаю, что это стоило бы в принципе запретить. Вместо того, чтобы просто использовать наследование, или протокол, мы просто подменяем селекторы "по-тихому". Это очень тяжело дебажить. Это должно быть отображено в документации (ха-ха). Каждый новый разработчик должен быть в курсе того, что swizzling имеет место быть и так далее. Слишком много проблем ради пяти минут лени.
Условная компиляция - механизм языка, который позволяет компилировать участки кода лишь только в том случае, если какое-то условие выполнено. Это достаточно низкоуровневый механизм, который использует флаги компиляции а так же определенные пользователем флаги для определения выполнения условия.
Существует так же ряд специальных деректив, которые расширяют возможности условной компиляции. К примеру, есть возможность определить под какую систему код компилируется в данный момент, происходит ли сейчас выполнение на настоящем устройстве, или на симуляторе, можем ли мы заимпортить тот или иной модуль и так далее.
S - single responsibility. Каждый элемент программы должен выполнять только одну роль.
O - open-closed. Элементы программы должны быть открыты к расширению, но закрыты к модификации.
L - Liskov substitution. Элементы программы, работающие с базовыми классами или протоколами, должны так же работать и с наследниками или реализаторами протокола.
I - Interface segregation. Множество специализированных интерфейсов лучше, чем один большой на все сразу.
D - Dependency inversion. Строить зависимости необходимо на интерфейсах, нежеле на конкретных типах.
Это еще один очень любимый и никому не нужный вопрос, ответ на который, по сути, гуглится ровно за 5 минут до собеседования.
Вы должны знать принципы SOLID и использовать их там, где это необходимо, но знать наизусть слово в слово определние каждого термина - это маразм.
В iOS три самых распространенных паттерна - это singleton
, delegate
и observer
. С недавних пор к ним присоединились publisher
и actor
.
Singleton
- пораждающий паттерн, который гарантирует единственность экземпляра конкретного типа. Основная проблема Singleton
- практически полная невозможность контролировать доступ и модификацию его состояния, так как он доступен всегда из всего кода.
Еще один вопрос, когда лучше просто написать сразу код.
final class Singleton {
static let shared = Singleton()
private init() {}
}
Я видел синглтоны, которые реализовывали через
struct
. В целом это допустимо, но накладывает целый ряд дополнительных трудностей в использовании такого синглтона.
Delegate
- поведенческий паттерн, который перекладывает часть реализации на другой объект. Делегаты используются в iOS сплошь и рядом в основном для того, чтобы уточнить часть информации, которая зависит от вашего кода и встроить в уже готовую реализацию используемых компонентов.
Примеры:
UITableViewDataSource
иUITableViewDelegate
Observer
- поведенческий паттерн, который позволяет наладить механизм подписки на события нескольким объектам и реагировать на эти события.
Пример:
NotificationCenter
.
7. 🟢 Назовите все парттерны, которые вы знаете. Перечислите те, которые вы использовали на практике.
В принципе сейчас, наконец-то, народ начал по-немного отходить от того, что знание всех паттернов в мире показывает хоть что-то, кроме того, что у вас хорошая память. Основные паттерны, приведенные выше, знать надо, потому что они используются абсолютно везде в iOS, но что до остальных - все весьма по желанию и опционально.
И да, все вопросы про паттерны как начинаются, так и заканчиваются на джунах, дальше это уже никому не интересно.
1. 🟢 Назовите все состояния, в которых может находиться iOS приложение (Application life cycle). Назовите примеры, когда может наступить каждое из состояний.
iOS приложение может находится в одном из нескольких состояний:
- Not running - приложение не запущено.
- Foreground inactive - приложение находится на экране, но не является first responder. Такое может произойти, например, когда ваше приложение перекрывает UI телефонного звонка.
- Foreground active - ваше приложение находится на экране и является first responder.
- Background - ваше приложение свернуто и не находится на экране, однако оно все еще работает и получает обновления от системы.
- Suspended - ваше приложение свернуто, не находится на экране и больше не получает обновления от системы.
Вопрос встречается часто, на практике данные знания применяются очень редко.
Вопрос довольно глупый, но он все равно бывает. В 99.9999% случаев вы работаете с Foreground Active состоянием.
Storyboard
, Xib
и Nib
- это, по своей сути, одно и тоде - XML
файл, который описывает UI приложения. Формат данного файла не имеет документации и данные файлы не предполагают редактирование за пределами Interface Builder
.
Xib
и Nib
не отличаются вообще ничем, это два названия для одного и тоже термина, на смену Nib
пришел Xib
. Xib
описывает UI отдельно взятой UIView
или нескольких UIView
.
Storyboard
отличается от Xib
тем, что имеет ряд дополнительных возможностей, так как описывает UI не UIView
, а одного или нескольких UIViewController
. В связи с этим Storyboard
может так же определять взаимодействия и переходы между разными UIViewController
.
Для построения UI iOS может использовать два фреймворка: UIKit
или SwiftUI
.
Данных два фреймворка отличаются очень многим, в деталях это расписывать очень долго. Если вкратце, то UIKit
существует испокон веков и именно на нем строится весь UI iOS приложения, там происходит его реальная имплементация.
SwiftUI
, в свою очередь, новый фреймворк, который не столько реализует UI (пока что), как описывает его, а конкретная реализация падает на плечи других фреймворков, в зависимости от того, на какой платформе данный код будет исполняться. На iOS все так или иначе опускается до UIKit
.
UIKit
- это UI фреймворк, который хранит в себе огромную массу функционала и возможностей для построения UI всего вашего приложения. Все, что будет отображено на экране и взаимодействовать с пользователем через экран, находится в UIKit
.
AutoLayout - это система внутри UIKit
, которая отвечает за размещение и размеры элементов на экране приложения. AutoLayout работает через constraints - элементы, которые являются частью уравнения, которое AutoLayout решает, когда размещает и задает размер ваших UI элементов на экране.
7. 🟢 Какое минимальное количество constraints надо задать, чтобы определить положение UIView
на экране?
Есть несколько вариантов ответа на этот вопрос:
- 2 constraints, одна по вертикали, а вторая по горизонтали, если
intrinsicContentSize
вьюхи определен. - 3 constraints, если
intrinsicContentSize
вьюхи определен только по одной оси. Тогда необходим дополнительный constraint по оси, по которойintrinsicContentSize
не определен. - 4 constraints, по две по вертикали и горизонтали, если
intrinsicContentSize
вьюхи не определен.
constraint имеет следующие аттрибуты:
firstItem
- первый якорь элемента, к которому относится данный constraint.secondItem
- второй якорь элемента, к которому относится данный constraint. Элемент может быть другим или тем же. Якорь может быть nil.constant
- константа constraint.multiplier
- множитель константы или значения якоря элемента.relation
- отношение между якорями. Может бытьequalTo
,greaterThanOrEqualTo
, lessThanOrEqualTo`.priority
- приоритет constraint. Находится в диапазоне от 0 до 1000.
9. 🟢 Что такое contentHuggingPriority
и contentCompressionResistancePriority
? Как их использовать и для чего?
contentHuggingPriority
- это приоритет, который говорит насколько вероятно, что UIView
станет больше своего необходимого размера.
contentCompressionResistancePriority
- это приоритет, который говорит насколько вероятно, что UIView
станет меньше своего необходимого размера.
Данный приоритеты используются вместе с приоритетами constraint для решения неопределенности, при которой две UIView
не могут определить свой размер.
UIStackView
- это UIView
, которая не имеет своего графического представления и используется исключительно для расположения своих subViews
по вертикали или по горизонтали.
Преимущества UIStackView
очевидны - меньше ручной настройки UI для достижения тривиальных задач. Недостатки вытекают отсюда же: если необходимо сделать немного более сложный UI, чем просто расположить элементы один за другим, UIStackView
может создать больше проблем, чем решить.
11. 🟢 Жизненный цикл UIViewController
. Назовите все методы-обработчики жизненного цикла и когда каждый из них вызывается.
У UIViewController
есть следующие методы-обработчики жизненного цикла:
viewDidLoad
- вызывается всего один раз для каждогоUIViewController
в течени жизненного цикла. Сигнализирует о том, что вью была загружена и теперь может быть использована.viewWillAppear
- вызывается перед тем, какUIViewController
будет показан на экране. Может быть вызван более одного раза за жизненный цикл.viewDidAppear
- вызывается после того, какUIViewController
был показан на экране и анимация его представления завершилась. Может быть вызван более одного раза за жизненный цикл.viewWillDisappear
- вызывается перед тем, какUIViewController
будет убран с экрана. Может быть вызван более одного раза за жизненный цикл.viewDidDisappear
- вызывается после того, какUIViewController
был убран с экрана и анимация его исчезновения завершилась. Может быть вызван более одного раза за жизненный цикл.
frame
- это прямоугольник, который представляет положение и размер UIView
в системе координат ее superView
.
bounds
- это прямоугольник, который представляет положение и размер UIView
в системе координат этой же UIView
. origin
bounds
всегда находится в координате (0, 0).
13. 🟢 Что такое UISegue
? Как они обрабатываются? Какие есть альтернативы и в чем преимущества/недостатки каждого из них?
UISegue
- это встроенный тип, который отвечает за переход между несколькими UIViewController
, объявленный в Storyboard
.
Чтобы обработать UISegue
, необходимо перегрузить метод prepare
.
Альтернативой использования UISegue
будет создание и презентация UIViewController
вручную в том месте, где необходимо выполнить переход с одного UIViewController
на другой. Как правило UISegue
не используют в силу того, что они всецело опираются на identifier
, который является просто строкой из-за чего работать с ним не очень удобно.
UINavigationController
- это UIViewController
, который является контейнером для других UIViewController
и осуществляет навигацию между ними. UINavigationController
имеет UINavigationBar
, который показывается в верхней части экрана, а так же имеет стандрартные анимации перехода между UIViewController
-ами, переход между которыми происходит в данный момент. Является одним из самый важных и основных компонентов UI.
UITabController
- это UIViewController
, который является контейнером для других UIViewController
и осуществляет переход между ними посредстом табов. UITabController
показывает таб бар в нижней части экрана, где у каждой табы может быть своя иконка и название. Является одним из основных компонентов UI, однако используется не всегда.
16. 🟢 Что такое UITableView
? Для чего его использовать и что необходимо сделать, чтобы его использовать?
UITableView
- UIview
, которая представляет собой вертикальный список, каждый элемент которого - это ячейка списка. UITableView
- это один из наиболее часто используюемых компонентов при построении UI приложения и его можно встретить практически в каждом приложении.
Чтобы использовать UITableView
, необходимо реализовать протокол UITableViewDataSource
. В этом случае вы можете отобразить данные, но не сможете с ними взаимодействовать. Если же необходимо обрабатывать пользовательское взаимодействие с таблицей, необходимо так же реализовать протокол UITableViewDelegate
.
У UITableViewDataSource
есть альтернатива - UITableViewDiffableDataSource
. В отличии от UITableViewDataSource
это не протокол, а конкретный тип, который реализовывает более современный подход к работе с таблицами.
UITableViewDiffableDataSource
в реальном коде встречается все еще не так часто, но, если вы с ним не знакомы, я настоятельно рекомендую освоить его и забыть проUITableViewDataSource
навсегда.
17. 🟢 Что такое UICollectionView
? Для чего его использовать и что необходимо сделать, чтобы его использовать?
UICollectionView
- UIview
, которая представляет собой коллекцию, состоящую из ячеек. Визуальное представление UICollectionView
зависит от его collectionViewLayout
и может быть абсолютно любым. Самый частый случай - это вертикальная или горизонтальная сетка элементов. Так же как и UITableView
является одним из наиболее часто используюемых компонентов при построении UI приложения.
Чтобы использовать UICollectionView
, необходимо реализовать протокол UICollectionViewDataSource
. В этом случае вы можете отобразить данные, но не сможете с ними взаимодействовать. Если же необходимо обрабатывать пользовательское взаимодействие с коллекцией, необходимо так же реализовать протокол UICollectionViewDelegate
.
У UICollectionViewDataSource
есть альтернатива - UICollectionViewDiffableDataSource
. В отличии от UICollectionViewDataSource
это не протокол, а конкретный тип, который реализовывает более современный подход к работе с коллекциями.
Так же, если вы хотите использовать collectionViewLayout
отличный от UICollectionViewFlowLayout
, его так же необходимо отдельно создать, настроить и указать коллекции.
UICollectionViewDiffableDataSource
в реальном коде встречается все еще не так часто, но, если вы с ним не знакомы, я настоятельно рекомендую освоить его и забыть проUICollectionViewDataSource
навсегда.
UITableView
способна отображать свои элементы исключительно как вертикальный список, в то время, как элементы коллекции могут отображаться в абсолютно произвольной форме. Современные нововведения в UICollectionView
позволяют полностью перестать использовать UITableView
.
GestureRecognizer
- это встроенный тип, позволяющий обрабатывать различные жесты пользователя. Они бывают следуюших типов:
UITapGestureRecognizer
- обрабатывает короткое нажатие.UIPinchGestureRecognizer
- обрабатывает двухпальцевый жест увеличения и уменьшения.UIRotationGestureRecognizer
- обрабатывает двухпальцевый жест поворота.UISwipeGestureRecognizer
- обрабатывает быстрый смах по экрану.UIPanGestureRecognizer
- обрабатывает жест перемещения пальца по экрану.UIScreenEdgePanGestureRecognizer
- обрабатывает жест перемещения пальца от края экрана.UILongPressGestureRecognizer
- обрабатывает жест длинного нажатия на экран.UIHoverGestureRecognizer
- обрабатывает жест наведения курсора на элемент UI. Доступен только на iPadOS.
20. 🟢 Что такое LaunchScreen.storyboard? Можно ли динамически менять его элементы или поставить ему класс-обработчик? Почему?
LaunchScreen.storyboard - это специальный storyboard, который показывается в момент запуска приложения. Содержимое данного storyboard нельзя динамически настраивать так же, как и UIViewController
не может иметь класс-обработчик, так как LaunchScreen.storyboard показывается до того, как метод application(_:didFinishLaunchingWithOptions:)
будет вызван.
Size class - это параметр, указывающий на то, в каком окружении в данный момент выполняется наше приложение. Есть два значения size class - по-вертикали и по-горизонтали. Комбинация этих значений может указать нам на диапазон устройст и состояний устройств, в которых мы сейчас исполняемся. Используется для построения различного UI в зависимости от доступного пространства на экране.
Size class может быть одним из следующих значений:
- compact
- regular
- unspecified
Как определить девайс и положение девайса по size class:
- [Width: Compact, Height: Regular]:
- все айфоны в портретном режиме;
- все айпэды в режиме overlay или splitView в соотношении 1/3 в ландшафтном или портретном режиме;
- все айпэды, кроме 12.9", в режиме splitView в соотношении 1/2 в ландшафтном режиме.
- [Width: Compact, Height: Compact]:
- все айфоны в ландшафтном режиме, кроме Plus/Pro Max айфонов.
- [Width: Regular, Height: Compact]: Plus/Pro Max айфоны в ландшафтном режиме.
- [Width: Regular, Height: Regular]:
- все айпэды в полноэкранном режиме;
- 12.9" айпэды в режиме splitView в соотношении 1/2 в ландшафтном режиме.
Знать наизусть какие девайсы принадлежат какой комбинации не обязательно, достаточно понимать, какая комбинация будет встречаться в каком случае чаще всего.
Для того, чтобы построить UI в коде, необходимо всю работу Interface Builder-а взять на себя. А именно, создать экземпляры необходимых UIView, настроить их нужным образом, добавить во view hierarchy и задать между ними constraints.
Преимущества данного подхода в том, что, во-первых, вы больше не связываетесь с Interface Builder. Больше не может случится ситуация, при которой в файле IB случилась непоправимая ошибка, в результате чего Xcode больше не может отрендерить UI в принципе и вам надо вручную искать ошибку в сгенерированном недокументированном файле. Во-вторых, мерж конфликты решать в коде значительно проще, чем в IB файлах.
Недостатки вытекают из достоинств: слишком много кода, который просто настраивает UI, наглядно не видно, что именно и как настроено, работать с size class становится немного сложнее.
Лично я был одно время горячим фаном UI из кода, но чем больше проходило времени, тем больше я понимал, что сверстать какую-нибудь маленькую ячейку быстрее и проще в IB, а вот сливать все в кучу в каком-нибудь сложном ViewController все же лучше руками, так надежнее. В результате сейчас я стараюсь комбинировать два этих подхода.
CALayer
является компонентом CoreGraphics
- низкоуровнего API, который непосредственно занимается рендером и анимациями. CALyaer
является наследником CGLayer
. UIView
является контейнером для CALayer
и у каждой UIView
есть как минимум один CALayer
, как у каждого UIViewController
есть как минимум один UIView
.
Для этого необходимо реализовать протокол UIViewControllerAnimatedTransitioning
, затем задать значение custom в поле modalPresentationStyle
. Значение полю transitioningDelegate
присвоить объект, который реализовал UIViewControllerAnimatedTransitioning
. Все данные поля необходимо присваивать объекту UIViewController
с которого необходимо реализовать кастомный переход.
25. 🟡 UISplitViewController
- что это, для чего используется и как реализован? Какие недавние изменения в нем появились?
UISplitViewController
- это UIViewController
, который является контейнером для других UIViewController
. UISplitViewController
располагает свои viewControllers
друг рядом с другом по горизонтали. Первая колонка по умолчанию будет значительно уже второй. Данное расположение доступно только для девайсов, которые имеют regular
size class
по горизонтали. При значении compact
size class
по горизонтали UISplitViewController
превращается в обычный UINavigationController
.
С недавних пор UISplitViewController
был обновлен. Ему полностью переписали API, добавили возможность сделать три колонки, а так же сделать полноценно отдельный UIViewController
для девайсов со значением compact
size class
по горизонтали.
layoutIfNeeded()
- метод UIView
, который инициирует полный перерасчет всего UI данной UIView
и всех ее subViews, если текущий UI не актуален.
setNeedsLayout()
- метод UIView
, указывающий UIView, что текущее отображение UI данной UIview
более не актуально и должно быть пересчитано.
layoutSubviews()
- метод UIView
, который инициирует полный перерасчет всего UI данной UIView
и всех ее subViews. В отличии от layoutIfNeeded()
не проверяет текущий UI на актуальность.
27. 🟡 Что такое UIResponder? Для чего он используется? Как происходит обработка событий UIResponder?
UIResponder - тип UIKit, который отвечает за обработку всех событий пользователя. Обработка событий UIResponder
происходит по цепочке ответственности. Если данный UIResponder
не может обработать то или иное событие, он пробрасывает событие далее своим дочерним UIResponder
. Так событие опускается до тех пор, пока не будет обработано, или, пока не закончатся UIResponder
-ы.
Вопрос с подвохом. Если мы говорим об операциях, которые непосредственно взаимодействуют с UI нашего приложения, который находится на экране или во вью стеке, но все подобные операции обязаны выполняться только на главном потоке. Без исключний.
Если же мы говорим о UI операциях в целом, то все операции, которые не взаимодействуют с UI во вью стеке, можно выполнять где-угодно. Другими словами, получить данные картинки и создать UIImage
можно на любом потоке, но присвоить UIImageView
поле image
можно только на главном потоке, если данная UIImageView
находится во вью стеке.
UIScene
- тип UIKit
, который появился в iOS 13. UIScene
отвечает за хранение UIWindow
приложения. UIScene
имеет смысл только на iPadOS. Данный тип позволяет осуществлять запуск более одного экземпляра приложения одновременно. Так же, благодаря UIScene
на iPadOS работает мультиоконный режим, при котором несколько приложений могут находится на экране одновременно.
30. 🟡 Сколько LaunchScreen сторибордов может быть в приложении? Если более одного, то как система определяет, какой использовать сейчас?
По умолчанию, при создании любого iOS приложения, Xcode сгенерирует один LaunchScreen.storyboard, который будет использовать при любом запуске приложения. Однако, вы можете создать и другие подобные storyboard-ы. При реализации URL схем и Push notification, вы можете указать какой конкретно LaunchScreen.storyboard вы хотели бы использовать, если при обработке схемы или уведомления приложение будет запускаться с нуля.
CADisplayLink
- тип фреймворка UIKit, который отвечает за синхронизацию с тактом рендера. CADisplayLink
используется для того, чтобы выполнять действия ровно каждый кадр.
SwiftUI
- новый фреймворк для UI, который не столько строит и рендерит сам UI, сколько описывает его и опирается на реальный функционал других API, в зависимости от того, на какой платформе будет исполняться код, написанный с помощью SwiftUI
.
Данный фреймворк все еще довольно молод и не позволяет воссоздать абсолютно все, что можно сделать на UIKit
/AppKit
, однако SwiftUI
позволяет внедрять UIKit
/AppKit
код в SwiftUI
код и наоборот.
View
- основной протокол фреймворка SwiftUI
. Все, что отображается на экране, будь то целый экран, или его элемент, должны реализовать данный протокол.
ViewModifier
- второй по значимости протокол в SwiftUI
. С его помощью мы можем модифицировать уже созданную View
, меняя текущие параметры или добавляя новые.
Из коробки в iOS нам доступны следующие хранилища: UserDefaults
, CoreData
, KeyChain
и запись в файл.
plist
файл в iOS - это файл, который содержит информацию в виде (ключ, значение), как и Dictionary
. Используется для различных целей, как правило, содержит набор настроек или информации, специфической для вашего приложения.
Пример:
Info.plist
UserDefaults
- одно из локальных хранилищ, которое хранит информацию в виде plist
файла. Данное хранилище не шифруется, следовательно не предназначено для хранения чувствительной информации. Так же данное хранилище не является базой данных и не подходит для хранения больших массивов данных, сложных связей между моделями и не имеет возможности составлять сложные запросы, как СУБД. Как правило UserDefaults
используют для хранения настроек приложения или флагов, которые даже при утечки не несут никакой угрозы бизнесу или пользователям приложения.
REST
- архитектурный подход, который чаще всего используется при построении клиент-севрверных приложений. Сервер и клиент общаются друг с другом посредством HTTP запросов. Клиент отправляет серверу запросы и сообщения с данными, получая данные в ответ, с которыми дальше и может работать. Связь между клиентом и сервером всегда инициирует клиент.
RESTful API
- это набор запросов, которые сервер может обработать и предоставить ответ на них от клиентов. Общение с сервером за пределами данного API возможно, но находится за пределами понимания REST
.
HTTP запросы могут быть следующих типов:
GET
- метод, использующийся для получения ресурсов с сервера.POST
- метод, использующийся для отправки новых ресурсов на сервер.PUT
- метод, использующийся для отправки новых или уже имеющихся ресурсов на сервер. Если данный ресурс на сервере уже имеется, он будет заменен новым полностью.PATCH
- метод, использующийся для частичного обновления уже имеющегося ресурса на сервере. В отличии отPOST
, ресурс не перезаписывается полностью, а обновляется только теми данными, которые указаны в запросе.DELETE
- метод, использующийся для удаления ресурса с сервера.
Скорее всего, чаще всего вы выпользуетесь
GET
иPOST
JSON
- формат данных, который представляет собой файл, содержащий информацию в виде { "ключ": значение }
. Данный формат является самым распространенным форматом сообщений между клиентом и сервером при построении клиент-серверных приложений.
На сегодняшний день парсинг JSON
в iOS довольно прост и делается чуть ли не в две строки. Чтобы распарсить JSON
, необходимо объявить тип, который будет хранить информацию из JSON
, реализовать протокол Decodable
данным типом, чтобы он мог парсить контейнер с данными, затем получить данные с сервера, и воспользоваться методом decode
типа JSONDecoder
.
Encodable
- протокол, реализация которого позволяет любому типу закодировать себя в формат выбранного кодировщика.
Decodable
- протокол, реализация которого позволяет любому типу раскодировать себя из формата выбранного декодировщика.
Codable
- объеденение Encodable
и Decodable
.
struct Model {
var name: String
var lastName: String
}
// Get data from somehwere
// JOSN format is
// {
// "name": "Mark",
// "lastName": "White"
// }
let data: Data = ...
do {
let model = try JSONDecoder().decode(Model.self, from: data)
print(model) // prints Model(name: "Mark", lastName: "White")
} catch {
// Handle parsing error here
}
URLSession
- встроенный в iOS тип, с помощью которого приложение может совершать запросы к серверу и получать сообщения в ответ. Вы можете создать свою сессию, а можете пользоваться сессией shared
, которая создается системой для каждого приложения.
URL
- встроенный тип, представляющий собой путь к ресурсу, как локальному, так и размещенному удаленно.
URLRequest
- встроенный тип, представляющий собой HTTP запрос, который вы можете отправить в сеть.
URL
и URLRequest
отличаются тем, что URLRequest
- это не просто путь к ресурсу, но сам запрос, который содержит путь к ресурсу, а так же информацию, как именно мы будем запрашивать данный ресурс и какое сообщение мы будем отправлять в сеть. URL
является частью URLRequest
.
Как правило вы не используете URL
напрямик с URLSession
, вы всегда создаете полноценный URLRequest
. Однако, использование URL
с URLSession
возможно, в таком случае система создат URLRequest
за вас. Это будет HTTP GET Request
с путем к ресурсу без дополнительной информации.
Счетчик ссылок - это механизм всех reference type
типов в Swift, благодаря которому reference type
типы работают так, как они работают. Так как при присваивании одному экземпляру reference type
типа другого экземпляра этого же типа, происходит не копирование реального объекта, а лишь ссылки на него, нам необходимо знать, когда на данный объект никто не будет ссылаться, чтобы мы могли его удалить. Счетчик ссылок как раз и считает ссылки на объект.
Важно уточнить, что ключевые слова weak
и unowned
не влияют на счетчик ссылок. Именно поэтому они всегда являются Optional
и именно поэтому удается решать проблему reference cycle
.
2. 🟢 Что такое reference cycle? Когда он может произойти? Что необходимо сделать, чтобы избежать таких ситуаций?
reference cycle - цикл ссылок или циклическая ссылка, ситуация, при которой один или более объектов указывают друг на друга. Данная ситуация может произойти по разным причинам, однако, самая частая из них - это неявный захват self
внутри замыкание, которое удерживает self
.
Чтобы избежать reference cycle, необходимо одну из ссылок сделать weak
или unowned
. Какую именно зависит от реализации программы и бизнес логики.
class ViewController: UIViewController {
@IBOutlet var titleLabel: UILabel!
let network = Networ()
override func viewDidLoad() {
super.viewDidLoad()
network.fetch { newTitle in
self.titleLabel.text = newTitle
}
}
}
В примере выше образуется reference cycle. ViewController
удерживает переменную network
. При вызове функции fetch(completionHandler:)
ее completionHandler
удерживает self (ViewController).
Для решения данной проблемы в список захвата completionHandler
self
необходимо передать как weak
class ViewController: UIViewController {
@IBOutlet var titleLabel: UILabel!
let network = Networ()
override func viewDidLoad() {
super.viewDidLoad()
network.fetch { [weak self] newTitle in
self?.titleLabel.text = newTitle
}
}
}
Обратите внимание, что при реализации делегата, переменная
delegate
так же должна бытьweak
. Это позволяет типу быть собственным делегатом и не указывать сильно самому на себя. Чтобы это было возможно любой протокол делегата должен быть ограниченAnyObject
.
iOS использует ARC модель. ARC - Automated Reference Counting - система, которая построена на факте того, что вы не будете работать с объектами напрямик, а будете работать со ссылками на эти объекты. При создании объекта так же создается и первая ссылка на данный объект, которая кладется в переменную/константу, которая инициировала создание объекта. При попытки скопировать такой объект, система будет создавать новую ссылку на уже существующий объект. Таким образом, когда количество ссылок на объект станет равным нулю, объект удаляется.
Через этот механизм реализованы reference type
типы Swift и весь ObjC Runtime iOS.
6. 🔴 Что такое ObjC Runtime? Что такое диспетчиризация методов, dispatch table? Назовите преимущества и недостатки.
Клиент-серверная архитектура - это подход разработки приложения, при котором есть приложение, которое выступает в роли клиента и сервер, с которым приложение общается, получая и отправляя туда данные.
MVC
- Model-View-Controller. Самая базовая архитектура приложения, которую в свое время пытались реализовать Apple, но получилось лишь отчасти.
MVC
состоит из:
Model
- данные приложения, с которыми мы работаем.View
- UI приложения и все, что взаимодействует с пользователем.Controller
- медиатор междуModel
иView
. Реагирует на измененияView
и действия пользователя, обновляяModel
; обновляетView
, при измененииModel
.
Apple MVC
грустно известен как Massive ViewController
. Изначального разделения между View
и Controller
не существует, из-за чего ViewController
делает слишком много работы и имеет тенденцию становится очень большим и трудно изменяемым. Использования Apple MVC
необходимо избегать при любой возможности.
MVP
- Model-View-Presenter. По сути, это тот же MVC
, но с менее путающими терминами. Так как в iOS есть ViewController
, но нет ViewPresenter
, намного легче представить ViewController
как View
, при этом полностью не запутавшись в том, кто и чем должен заниматься.
MVP
состоит из:
Model
- данные приложения, с которыми мы работаем.View
- UI приложения и все, что взаимодействует с пользователем.Presenter
- медиатор междуModel
иView
. Реагирует на измененияView
и действия пользователя, обновляяModel
; обновлетView
, при измененииModel
.
MVVM
- Model-View-ViewModel. Еще одна вариация MVC
, но уже с реактивным подходом (как правило). Данная архитектура характерна тем, что обновления получаются посредством канала связи, а не запросом на состояние.
MVVM
состоит из:
Model
- данные приложения, с которыми мы работаем.View
- UI приложения и все, что взаимодействует с пользователем.ViewModel
- медиатор междуModel
иView
.ViewModel
подписывается на обновленияModel
, обновляяView
, а так же подписывается на событияView
, обновляяModel
.
MVVM
можно реализовать и без реактивного программирования, но тогда это будет больше похоже наMVP
VIPER
- View-Interactor-Presenter-Entity-Router. VIPER
- это попытка создать кольцо всевластия. Единая архитектура, которая хороша во всем и решает все проблемы человечества. К сожалению, мы живем в неидеальном мире и ничего подобного быть не может, так что и у VIPER
есть свои недостатки.
VIPER состоит из:
View
- UI приложения и все, что взаимодействует с пользователем.Interactor
- Обработчик всех событий и изменений, реализующий всю бизнес логику приложения.Presenter
- компонент, отвечающий за то, как именно должен обновиться View (состояние, анимации, переходы в пределах одногоVIPER
модуля).Entity
- данные приложения, с которыми мы работаем.Router
- компонент, отвечающий за переход между разнымиVIPER
модулями.
Как правило один VIPER
модуль - это один экран приложения, но данное правило не является строгим.
Про
VIPER
довольно часто любят спрашивать, но слава богу, его практически не используют, по крайней мере в чистом виде.
Проблемы с VIPER начинаются ровно тогда, когда вы пытаетесь реализовать на нем любой проект, пытаясь соблюдать VIPER от и до. Каждый экран, фича, компонент потребуют своего VIPER модуля, что вынуждает вас писать очень много поддерживающего кода для даже самой маленькой реальной функциональности, просто, чтобы соответствовать архитектуре.
В принципе, это касается любого подхода, просто с VIPER это максимально наглядно. Именно по-этому лучше относится к архитектурам не как к правилам и законам, а как к рекомендациям, применение которых всегда должно быть оправдано.
Из описания ответов на вопросы про MVC, MVP и MVVM можно сделать вывод, что это практически одно и тоже, с разницей на имена и стиль программирования. VIPER же отличается от них тем, что пытается решить те вопросы, которые MVC/MVP/MVVM просто не покрывает.
Здесь вы можете привести примеры других архитектурных подходов, с которыми вам доводилось работать, или недостатки которых вы знаете.
Здесь правильного ответа нет. Просто называете то, как бы вы реально выбирали подходы и чем бы руководствовались.
Совет: не пытайтесь здесь блистать знаниями. Человек, который будет делать что-то очень сложное, навороченно, гибкое и динамическое при любом раскладе, вызывает куда больше вопросов, чем человек, который отталкивается от реальных условий и требований и выбирает тот подход, который лучше всего ляжет на то, что у него есть.
Вы можете перечислить любые, которые знаете, но дебаггер и логгирование должны быть обязательно.
Логгирование - механизм фиксации и отображения текущего состояния выполнения приложения.
Чаще всего встречаюся следующие уровни логгирования:
debug
- с таким уровнем записываются сообщения, имеющие смысл только при отладке программы.info
- с таким уровнем записываются сообщения, несущие вспомогательную информацию.default
/verbose
- с таким уровнем записываются сообщения, несущие основную или важную информацию.error
/warning
- с таким уровнем записываются сообщения, несущие информацию об ошибке или предупреждении, не завершающие работу программы.fault
/error
- с таким уровнем записываются сообщения, несущие информацию об ошибке, в результате которой дальнейшее выполнение программы невозможно.
llvm - это интерфейс дебаггера, который мы можем использовать через командную строку во время отладки внутри Xcode. llvm имеет множество команд, которые помогают отследить и найти ошибку в программе или отобразить информацию о текущем состоянии программы.
llvm знать весь, конечно же, не обязательно, но хотя бы основыне команды знать желательно, ибо все мы знаем как хорошо работает Xcode.
View Hierarchy Debugger - это инструмент, встроенный в Xcode, который позволяет захватить текущий UI с экрана и разложить его на слои, предоставляя возможность визуально оценить верность отображения UI относительно дизайна, а так же отследить ошибки или не соответствия.
Вы можете использовать все, что угодно, но про View Hierarchy Debugger вы точно должны упомянуть. Все остальное - плюс в копилочку скиллов и карму.
Система контроля версий - это программа или утилита, которая позволяет хранить ваш код а так же записывать все ваши изменения в нем. Помогает не только в распространении кода между разработчиками, но и для отслеживания изменений во времени, слиянии разного кода в одном и том же участке.
Вам должно быть известно как минимум про git, остальные СКВ по желанию.
Рассмотрим данные команды на примере git:
push
- команда, которая отправляет ваши текущие закоммиченные изменения в репозиторию на удаленный репозиторий (remote)pull
- команда, которая проверяет наличие новых коммитов на удаленном репозитории и скачивает их, если они присутствуютfetch
- команда, которая проверяет наличие новых коммитов на удаленном репозитории, но не скачивает ихmerge
- команда, которая сливает две вертки репозитория в одну. При выполнении данной команды могут возникнуть конфликты.
Git Flow
- это порядок взаимодействия разработчиков с репозиторием, призванный минимизировать риски при выкатывании новых версий программы.
Как правило Git Flow описывает систему релиза вашей программы через релиз ветки, по следующей схеме:
При подготовке нового релиза программы создается релиз ветка.
Для каждой новой фичи данного релиза создается ветка фичи от ветки релиза.
При выполнении фичи, ветка фичи мержится в ветку релиза, тем самым выполняя требования данного релиза.
При достижении всех требований релиза, тестирования и прочих проверок, ветка релиза мержится в master/main ветку репозитория.
Pull/Meger Request
- это механизм web интерфейска удаленного репозитория, при котором мы не мержем изменения из одной ветки в другую, а создаем запрос на такой мерж. Web интерфейс позволяет другим участникам репозитория увидеть изменения, который собираются быть смержены перед тем, как они будут смержены.
Процесс оценивания данных изменений называется Code Review. В ходе Code Review могут быть предложены правки, предложения, выставлены замечания к автору запроса.
.gitignore файл - это специальный файл, который использует git, в котором описываются все файлы и папки, которые не должны попадать в репозиторий. Шаблон такого файла для любого языка/платформы с легкостью находятся в интернете, а Xcode, при создании репозитория через него, с недавних пор и сам научился создавать этот файл за вас со всем, что там должно быть для любого iOS приложения.
Однако, это не значит, что вы не можете/вам не понадобится добавлять туда что-то свое.
Merge
- это команда слияния двух веток в одну, в результате чего создается новый коммит. При этом вся история коммитов сохраняется.
Rebase
- это команда, которая просто форсированно меняет родительскую ветку вашей ветки на указанную, сливая изменения воедино. В результате данной операции коммит не создается.
При выполнении этих команд у вас могут возникнуть конфликты, ситуации, при которых, один и тот же участок кода был модифицирован в обоих ветках и какой из вариантов выбрать система решить автоматически не может. Чтобы решить эти конфликты лучше всего использовать любую программу или утилиту, которая позволит вам увидеть разницу между двумя ветками и решить какой код использовать. На сегодняшний день практически любая программа, которая имеет встроенный git, умеет это делать. Даже Xcode.
Unit тесты - отдельные функции или целые подпрограммы, проверяющие корректность работы вашего кода. Unit они называются от того, что каждый тест/набор тестов должен проверять отдельно взятый фрагмент/модуль/unit программы.
Это очень сложная тема с точки зрения комьюнити. Лично я тесты не очень люблю и практически никогда не пишу их, в то время, как есть люди, которые буквально не будут с вами общаться, если вы не исповедуете тестирование.
Stub - обычных тестовый объект. При использовании его все тесты должны проходить.
Mock - умный Stub. Тесты должны проходить не только используя данный объект, но и задействуя его.
9. 🔴 Если в приложении нет ни одного теста, как вы будете расставлять приоритеты для написания unit тестов? Будете ли вы писать тесты в принципе?
Dependency manager - это программа или утилита, которая упралвляет зависимостями, которые использует ваш проект.
На сегодняшний день существуют три самых популярных менеджера зависимостей:
- CocoaPods
- Carthage
- Swift Package Manager (SPM)
С появлением SPM, остальные вроде как и не нужны больше, особенно, если у вас мак на Apple Silicon, но, к моему сожалению, переход происходит не достаточно быстро и все еще очень многие не используют SPM.
Кстати, если вы затягиваете зависимости руками или копируете чужой код себе в репу, об этом тоже можно упомянуть.
CocoaPods - это dependency manager, утилита, которая помогает работать с внешними зависимостями и добавлять их в ваш проект.
Чтобы использовать CocoaPods, необходимо сначала скачать их любым удобным для вас способом и затем использовать их интерфейс через терминал.
Сами зависимости и настройки проекта описываются в файле Podfile.
Если вы используете мак с Apple Silicon, то любой удобный для вас способ только один - homebrew. Установив CocoaPods другим способом у вас могут быть проблемы с их запуском без rosetta.
Carthage - это dependency manager, утилита, которая помогает работать с внешними зависимостями и добавлять их в ваш проект.
Чтобы использовать Carthage, необходимо сначала скачать их любым удобным для вас способом и затем использовать их интерфейс через терминал.
Сами зависимости и настройки проекта описываются в файле Cartfile.
Swift Package Manager - это dependency manager, утилита, которая помогает работать с внешними зависимостями и добавлять их в ваш проект.
На сегодняшний день, чтобы использовать Swift Package Manager, вас не нужно ничего устанавливать или настраивать дополнительно. Все зависимости SPM описываются прямо в файле проекта, настраиваются через интерфейс Xcode, там же скачиваются и добавляются в зависимости проекта.