Привет! Я Александра Башкирова, iOS-инженер в Clover и старший код-ревьюер на курсе «iOS-разработчик» в Яндекс Практикуме. На момент подготовки статьи мы уже проверили более тысячи студенческих работ и успели заметить повторяющиеся ошибки.
В этой статье я разберу несколько популярных ошибок начинающих iOS-разработчиков, чтобы вы могли не повторять их в своей работе. Мы вместе посмотрим, к чему они приводят и как их можно избежать. Начнём с вёрстки, а потом посмотрим на код и выработаем стратегии для хороших практик.
Часть 1. Вёрстка
Мы учим студентов верстать в Storyboard — это самый быстрый способ получить готовое и работающее приложение. Вам не нужно особых знаний для того, чтобы создать несколько стандартных элементов в Interface Builder и расположить их как нужно. Интерфейс достаточно простой и чем-то даже напоминает создание работ в Paint.
Связь между Storyboard и кодом
Многим новичкам в iOS-разработке непросто понять взаимосвязь элементов Storyboard с кодом. Важно знать, что из себя представляет эта связь и за чем стоит следить в первую очередь.
Связывание графических элементов с кодом происходит с помощью аутлетов (outlets) и экшенов (actions). @IBOutlet
— это ссылка на графический элемент (например, кнопка или текстовое поле) в классе вью-контроллера или вью, которая позволяет получить доступ к его свойствам и методам. @IBAction
— это метод в классе вью-контроллера или вью, который вызывается при определённом действии пользователя (например, при нажатии на кнопку).
Допустим, если вы добавили кнопку на экран в Storyboard, то вы должны связать её с кодом, чтобы обработать нажатие. Если вы хотите поменять текст в лейбле, понадобится аутлет, чтобы поменять свойство text
.
Ошибки и решения
Научившись связывать элементы с кодом, можно уже уверенно написать простенький счётчик шагов или приложение с приветствием пользователей. Но что произойдёт, если мы захотим немного поправить код — например, переименовать переменную или удалить лишнюю функцию? Если сделаем такие изменения только в коде и попробуем запустить приложение, увидим ошибку:
this class is not key value coding-compliant for the key helloLabel
.
Приложение упало в runtime и сообщило, что не нашло подходящего значения для ключа helloLabel
.
В примере я переименовала helloLabel
в label
helloLabel
— это название элемента в момент, когда мы создавали связь со сторибордом. Дело в том, что в сториборде эта связь осталась неизменной, хотя во вью-контроллере тот же элемент уже называется label
. Storyboard вполне текстовый документ, его можно открыть в виде XML-документа и найти лейбл, описанный в таком блоке:
<connections>
<outlet property="helloLabel" destination="jeX-w7-wVM" id="hGw-e6-vqM"/>
</connections>
Получается, что Storyboard не узнал о том, что мы поменяли название ссылки на элемент. Чтобы решить эту проблему, нужно привести название к одинаковому состоянию в коде и в Storyboard. Для этого можно удалить старую связь с аутлетом в Connection Inspector и создать новый аутлет либо поменять название в коде на то, которое понимает ваш Storyboard.
Когда вы меняете название аутлета или экшена в коде, нужно гарантировать, что это же название поменялось и в вашем Storyboard. К счастью, таких проблем можно избежать, если для изменения названий пользоваться встроенным в Xcode рефакторингом Refactor → Rename — тут Xcode подскажет, что нужно обновить имя ссылки в сториборде тоже.
Ошибки нас ждут также, если удалим в Connection Inspector связь у Storyboard с кодом, но продолжим обращаться к оставшимся аутлетам из кода. @IBOutlet
создаётся неявным опционалом, потому что сама инициализация элемента не может быть сделана в момент создания контроллера. Инициализация будет выполнена системой позже, а если такой связи на сториборде нет, то не будет выполнена никогда.
Попробуем обновить значение в helloLabel
, но удалим связь в Storyboard: как только приложение дойдёт до строчки с установкой значения — произойдёт краш в runtime. Мы попробовали установить свойство text
несуществующему объекту, и такое обращение было небезопасным, ведь свойство было неявным опционалом.
В этом примере Xcode подсказывает пустым кружочком рядом с аутлетом, что связи с элементом нет. Если мы хотим поменять значение лейбла на Хабр, привет!
, то нужно будет восстановить ссылку на него, добавив связь заново. Например, соединив кружочки напротив new referencing outlet со свойством helloLabel
.
Итак, если вы столкнулись с runtime-ошибкой, которая упоминает ваш аутлет или экшен, обязательно проверьте:
Совпадение названий элемента или функции в коде и в сториборде.
Наличие связи с кодом у элемента на вкладке Connection Inspector.
Использование Auto Layout
Пожалуй, самая типичная ошибка новичка — создать красивый интерфейс для новенького iPhone Pro Max, но не адаптировать его под размеры других девайсов.
Адаптивный интерфейс верстаем с помощью Auto Layout. В Storyboard есть целый набор различных способов, как привязать элементы к краям других элементов или ограничить размеры View.
На старте может быть сложно подружиться с Auto Layout. Ведь важно не только научиться пользоваться базовыми инструментами, но и понять, почему Auto Layout работает именно так и как заставить его делать то, что вам нужно.
Важное свойство системы неравенств: она может иметь несколько решений. Именно поэтому возникают ошибки и предупреждения в Interface Builder, а также предложение «Добавить недостающие констрейнты». Из-за этого можно получить совершенно неожиданный результат: предполагаешь одно конкретное решение, а в ходе расчёта Auto Layout может подобрать совершенно другое решение неравенств :)
Изучив основы Auto Layout, можно переходить к работе с Figma.
Часто замечаю, что новички начинают верстать экран с фиксации размеров элементов. Это не лучшее решение, потому что элементы могут по-разному помещаться на разных экранах в зависимости от содержимого. Например, текст или изображение могут самостоятельно определять свои размеры по содержимому и выставленным настройкам. В этом случае правильнее ориентироваться на отступы элементов относительно других элементов и краёв экрана.
Например, на скриншотах выше можно задать серой View размер 335×502, что будет попадать в макет для iPhone X, но, вероятно, это будет многовато для iPhone SE 1-го поколения. Поэтому нужно определить в макете отступы у серой View сверху, слева, справа и снизу относительно блока с текстом. Или можно задать отступ сверху от края экрана 590, а не от соседней серой View, что точно вытолкнет её на самый низ экрана для iPhone 8 (а у нас там ещё кнопки должны расположиться). Поэтому правильно в этом случае задавать отступ для текста сверху от серой View, а снизу — от кнопок.
Верстать адаптивный интерфейс действительно непросто, но программисту не нужно самостоятельно принимать решение, как должны работать констрейнты при масштабировании экрана, — в этом ему помогает дизайнер. Удачная практика — создавать дизайн-макеты, которые будут хорошо выглядеть и под большой, и под маленький экран. В Figma есть две версии вёрстки — по ним можно определить, какие элементы могут сжиматься или что экран можно скроллить.
Пара слов о горизонтальных отступах
Новичкам сложно запомнить названия leading и trailing для горизонтальных привязок слева и справа. Возникает вопрос, почему Apple не назвала их left и right? На самом деле у Auto Layout есть привязки (они же anchors), которые так и называются left и right. Но Apple не просто так ввела понятия leading и trailing для описания границ элементов. Положения leading и trailing могут быть различными в зависимости от текущей Locale, и для арабской локали или иврита приложение с помощью таких констрейнтов будет корректно отображено с зеркальным размещением элементов. Если же вы будете использовать привязки left и right, то Auto Layout не будет переворачивать интерфейс для RTL-локалей. Мы советуем прислушиваться к рекомендации Apple и использовать leading и trailing, а запомнить их можно так: leading — ведущий, а trailing — замыкающий. Ведущим мы начинаем верстать, а замыкающим — заканчиваем.
И пара слов о вертикальных отступах
Уже несколько лет Apple выпускает iPhone с чёлкой и закруглёнными краями экрана, на которых расположены элементы системы Status Bar и Home Indicator. Пользователю должно быть удобно с ними взаимодействовать. Это означает, что мы, как разработчики приложений, не должны размещать свой UI в этих областях, чтобы не мешать пользователю. Для этого на вью экрана существует специальный гайд (layout guide), который описывает безопасную область — Safe Area.
Обычно у новичков с этим проблем нет, ведь в сториборде практически все констрейнты по умолчанию будут привязываться не к границам вью экрана, а сразу к Safe Area. Трудность заключается в интерпретации отступа в Figma. В примере выше есть отступ в 34 пункта у кнопок до края экрана, и можно невнимательно поставить один из констрейнтов:
34 пункта от нижнего края View,
34 пункта от нижнего края Safe Area.
И оба варианта неверны.
Первый вариант даст небольшую свободную область: там расположится Home Indicator. Но это заблуждение, во-первых, потому что существуют девайсы с прямоугольным экраном и физической кнопкой Home, для которых это пространство будет ненужным. Во-вторых, нет гарантии, что Apple не поменяет интерфейс iOS и размер под безопасное пространство рядом с Home Indicator, поэтому жёстко фиксироваться на значении 34 нельзя.
Второй вариант происходит из-за невнимательности: по умолчанию сториборды делают любую привязку к краям главной View экрана сразу к Safe Area, и поставить значение 34 — значит увеличить область снизу в два раза (верно для девайсов, у которых эта область уже 34).
Поэтому разработчикам нужно быть внимательными к вертикальным отступам у экрана в области Safe Area и в Figma обязательно отделять визуально отступы от неё, а не от края макета. В примере выше кнопки прикреплены к краю Safe Area, и их стоит прикрепить к ней с нулевым отступом.
Вёрстка в iOS может показаться лёгкой, но важно учитывать всю линейку девайсов и их размеры, а также особенности устройства инструментов для вёрстки. Сталкиваться с такими сложностями в начале пути — нормально. Помните, что верно свёрстанный экран — ответственность не только ваша, но и дизайнеров, не стесняйтесь спрашивать и привлекать к совместной работе коллег.
Часть 2. Кодинг
Использование фишек языка не по назначению
Наши студенты сначала учат основы Swift и применяют знания по мере создания различных приложений. На начальном этапе будущий джун часто собирает свой код из громоздких конструкций — рассмотрим некоторые из них.
Избыточный force unwrapping
var lastPage: Int? = nil
lastPage! += 1 // Произойдёт ошибка: операция на nil не может быть выполнена
Swift — типобезопасный язык. В нём достаточно красивых способов написать аккуратный код без использования force unwrap. В сообществе iOS-разработчиков принято писать безопасный код и избегать force-операций, ведь они могут привести к сбою приложения, и оно будет вылетать. Пока вы изучаете разработку под iOS, будьте бдительны в следующих ситуациях:
Когда Xcode предлагает «исправить» код на операции с восклицательным знаком: это и есть force-операции, их стоит избегать и переписывать на безопасные конструкции.
Присмотритесь к различным туториалам — там часто встречается код с force unwrap, но в учебных материалах force unwrap удобен именно для упрощения объяснения концепции и быстрого старта.
Вы можете отмахнуться от этого совета: «Ничего страшного не произойдёт в моём пет-проекте!» Да, не произойдёт. Относитесь к этому не только как совету писать безопасный код, но ещё и как к формированию best practice. В тестовых заданиях и на coding-интервью работодатели особенно пристально следят за форсами и могут уже на этом этапе забраковать вашу кандидатуру. Лучше сразу привыкать писать код так, как принято в сообществе.
Итак, если вы разобрались, что обязательно нужно проверить, безопасны ли операции на опционалах, вы можете написать что-то подобное:
var lastPage: Int? // Заведём некоторую опциональную переменную
// Проверим её на nil и в тернарном операторе
// возьмём ноль, если значение nil,
// или прибавим единицу, если значение существует
let nextPage = lastPage == nil ? 0 : lastPage! + 1
Это действительно безопасный вариант кода, но это тот случай, когда без него можно обойтись. Мы рекомендуем тренировать привычку использовать возможности языка:
Optional Binding — самый используемый способ, когда мы распаковываем элемент, тем самым сразу проверяя на опциональность:
// Объявим опциональную переменную. Если не присвоить в неё значение, то по умолчанию будет присвоен nil
var lastPage: Int?
// 1. Можно распаковать в новую константу и безопасно использовать константу внутри блока if
if let unwrappedLastPage = lastPage {
// используйте unwrappedLastPage здесь
}
// 2. Часто используют то же имя, создавая неопциональную копию значения, если значение имеется :)
if let lastPage = lastPage {
// несколько удобнее, так как не добавляет лишний мусор в нейминг (unwrapped)
}
// 3. Можно неявно определить с таким же именем, доступно для Swift 5.7
if let lastPage {
// Этот вариант технически аналогичен предыдущему, но заметно короче!
}
Изначальный пример мы можем написать так:
var lastPage: Int?
let nextPage: Int
if let lastPage {
nextPage = lastPage + 1 // Избавились от force unwrap
} else {
nextPage = 0 // использовали начальное значение
}
Nil Coalescing Operator — позволяет взять дефолтное значение в случае опциональности:
let value = optionalValue ?? defaultValue
Снова попробуем переписать наш изначальный пример:
var lastPage: Int?
// Возьмём значение, если оно есть, и прибавим единицу, чтобы получить следующую страницу
// Возьмём -1 в случае nil, прибавим единицу — получим индекс первой страницы, 0
let nextPage = (lastPage ?? -1) + 1
Guard Statement — позволяет прервать выполнение блока, если значение опционально.
guard let lastPage = lastPage else {
return
}
// используйте lastPage здесь
guard let currentPage else { // Короткий вариант в Swift 5.7, аналогичный if let
return
}
// используйте currentPage здесь
Если вы работаете с опциональным значением, сначала попробуйте подобрать подходящую стратегию. Нужно ли в этом случае выполнять какой-то код, или вы можете заменить его предзаданным значением? Стоит ли показать ошибку? Смело подбирайте подходящую конструкцию, в большинстве случаев ваш код можно написать, используя их.
Force unwrap можно использовать в редких ситуациях, если вы на 100% уверены, что переменная никогда не будет зависеть от внешнего окружения и не превратится в optional.
Например, константа с указанием адреса:
let appleURL = URL(string: "<https://www.apple.com>")!
Здесь мы, во-первых, уверены, что описали верную ссылку на сайт, во-вторых, это константа, и её значение статично независимо от состояния приложения.
Однако если мы немного изменим этот код, используя внешний аргумент, то гарантировать то же самое нельзя. В таком случае стоит переписать код на безопасный вариант разворачивания опционала:
var website = "https://www.apple.com"
var url: URL {
URL(string: website)!
}
print(url) // "https://www.apple.com" – Ух! Пока работает (потому что ссылка верная)
website = "one more thing..." // Перезапишем значение переменной другой многозначительной строкой
print(url) // Ошибка! Форс анврапнули внутри блока var url, всё взорвалось!
guard
Этот оператор — мой любимый в Swift. Конструкция нужна для проверки условий и распаковки опциональных значений с использованием выхода из функции или блока, если условия не выполнились. Этот оператор удобно использовать, чтобы распрямить код, то есть сделать его менее вложенным.
Однако новички любят условный оператор if и используют его очень активно:
func processName(name: String?) {
if let name = name {
// делаем что-то с именем
print("Привет, \(name)!")
}
}
Получился вполне безопасный вариант, ведь аргумент name
распакован в константу с помощью if let
, однако вложенность такого кода равна двум: один за функцию, два за блок if
. Если нам понадобится проверить ещё и длину имени, чтобы вывести фразу, то вложенность сразу возрастёт до трёх:
func processName(name: String?) {
if let name = name {
// делаем что-то с именем
print("Привет, \(name)!")
if name.count < 3 {
print("\(name), какое короткое у тебя имя!")
}
}
}
Разработчики не любят глубокую вложенность, потому что такой код сложнее тестировать, читать и в нём легче ошибиться. Поэтому всячески стараются распрямлять код — например, с помощью guard
:
func processName(name: String?) {
guard let name = name else {
return
}
// делаем что-то с именем
print("Привет, \\(name)!")
guard name.count < 3 else {
return
}
print("\\(name), какое короткое у тебя имя!")
}
С помощью guard
мы уменьшили вложенность кода и не потеряли в функциональности, а читать такой код стало проще, каждой проверкой мы отсекли лишние случаи. До конца функции мы дойдём только в случае, когда имя не опциональное и короткое: то есть мы описали идеальный путь работы функции, как будто никаких опционалов и неверных условий не произойдёт, а если произойдут, то мы просто не выполним никаких лишних операций.
Итак: используйте guard
для распрямления кода и построения позитивного сценария работы функции.
Иногда guard
из лаконичного оператора превращается в раздутый if
из первого примера:
func handle(message: Message?, error: String?) {
guard error == nil else {
// сообщаем об ошибке и выходим!
print("Упс! \\(error!)!")
removeHistory()
fastLogout()
return
}
guard let message else { return }
// делаем что-то ещё
}
Мы уже посмотрели, как можно избегать force unwrap, и в данном случае с error!
стоит поступить так же: аккуратно распаковать. Обратите внимание на раздутый блок guard else
. Блок else
в этом случае не предполагает выполнения сложных действий. Если вы обнаружили в блоке guard else
большой код, то, возможно, ваша функция делает слишком много. Поэтому код, предложенный выше, можно разбить на несколько дополнительных функций:
func handle(message: Message?, error: String?) {
if let error = error { // Распаковали error, чтобы избавиться от force unwrap
processError(error)
} else if let message = message {
processMessage(message)
}
}
func processError(_ error: String) {
print("Упс! \(error)!")
removeHistory()
fastLogout()
}
func processMessage(_ message: Message) {
// Обрабатываем сообщение
}
Этот пример кода имеет и архитектурную проблему: функция принимает два параметра, хотя предполагается, что только один из них будет неопциональным.
Возможны ситуации с другими вариантами: message
и error
— nil, тогда ничего не выполнится. Ещё один вариант: когда есть и ошибка, и сообщение — непонятно, как верно обработать эту ситуацию. Если есть ошибка, сообщение будет проигнорировано. В данном случае в Swift нам доступен специальный generic-тип Result<Value, Error>
, который и поможет исключить эти случаи:
guard
— очень удобный оператор ветвления. Мои рекомендации по нему:
Используйте
guard
для проверки значений опциональных переменных на nil.Помещайте
guard
как можно ближе к началу функции или блока.Используйте
guard
, чтобы отсечь ситуации, когда условия не выполняются. Так вы предотвратите глубокую вложенность кода.Используйте блок
else
для обработки случаев негативных сценариев: это может быть выход из тела функции, ошибка или другое поведение.Следите за размером блока
else
: если в нём получилось много кода, то, возможно, пора рефакторить.Используйте
guard
только для проверки условий, которые могут вызвать проблемы или ошибки в дальнейшем коде. Помните о подходе с позитивным сценарием функции.
switch
Этот оператор можно использовать для проверки значений различных типов данных, таких как числа, строки, перечисления (enum), булевы значения и другие. switch
включает в себя блоки case
, каждый из которых содержит определённое значение, которое нужно проверить и каким-то образом обработать. Давайте посмотрим на следующий пример, который я часто встречаю у студентов.
switch isAvailable {
case true:
print("Достпуно!")
case false:
print("Ограничено")
}
Новичок использует switch
для описания поведения для булева значения. В данном случае конструкция простая и легко читается. Однако лучше использовать вариант с if else
, потому что он чаще употребляется и гораздо легче кастомизируется, если понадобится добавить дополнительные проверки:
if isAvailable {
print("Достпуно!")
}
else {
print("Ограничено")
}
Рассмотрим ещё пару особенностей использования switch
. Допустим, есть switch
, обрабатывающий стороны света:
switch direction {
case .west: goToWest()
case .east: goToEast()
case .north: break
default: break
}
В блоке case .north: break
приложение не делает ничего, так как используется break
, что может быть неочевидным для других разработчиков, которые будут читать этот код. Вы можете добавить причину в виде комментария. Также можно обратить внимание, что в данном случае мощность switch
использована не полностью: только два направления обработаны, — и вы могли бы упростить код, использовав default
и для north
тоже:
switch direction {
case .west: goToWest()
case .east: goToEast()
default:
// Ничего не делаем
break
}
Использование варианта default
хорошо для случаев, когда мы описываем фиксированное количество кейсов и можем гарантировать, что не собираемся добавлять новые элементы в enum’ы. Однако enum’ы в разработке используются очень часто, и возникает потребность их менять и дополнять новыми кейсами. Что произойдёт с этим кодом, если вы добавите ещё четыре промежуточные стороны света? default
по умолчанию сделает всё за вас: пропустит их обработку. Это может быть неудобно, если подобных свитчей в приложении много, и было бы здорово сразу поправить код везде, где это требуется. Если мы откажемся от default
, то при добавлении сценария компилятор выдаст ошибку и попросит описать все сценарии. Знание этой особенности позволяет сэкономить время на отладке. И исходя из этой мысли стоит переписать свитч с полным перебором вариантов:
switch direction {
case .west: goToWest()
case .east: goToEast()
case .north, .south:
// Если направление на север или на юг, то ничего не делаем
break
}
На что стоит обратить внимание при работе со switch
:
Использование оператора не должно быть избыточно, и если его легко можно переписать с помощью
if
, то лучше отказаться отswitch
.Перечислять все возможные случаи, чтобы убедиться, что ничего не упущено, и иметь возможность заметить ошибку при дополнении новыми случаями.
Использовать блок
default
, чтобы обработать неожиданные значения или ошибки, но не для пропуска очевидных значений.
Ошибки при реализации паттерна delegate
Начинающим разработчикам непросто даётся понимание делегирования, хотя это самый часто используемый паттерн проектирования в iOS-разработке.
Представьте: в магазин ворвался грабитель, продавец нажимает тревожную кнопку под столом и вызывает охрану, то есть делегирует ответственность борьбы с нарушителем. В этом случае грабитель — это произошедшее событие, которое нужно обработать. В роли делегата выступает охранник, а продавец магазина играет роль объекта, который делегирует выполнение функции охраны магазина.
Аналогично в программировании: делегирование используется для передачи ответственности от одного объекта к другому. Например, если объект A не может выполнить определённую функцию, он может делегировать эту функцию объекту B, который имеет необходимые навыки и знания для выполнения этой функции.
В Swift паттерн реализуют с помощью протокола с описанием делегируемых действий и ссылочной связи между объектами. Объект, который будет делегировать свои действия, имеет ссылку на объект-делегат, который должен реализовать соответствующий протокол. Когда происходит событие, объект, который хочет делегировать свои действия, вызывает соответствующий метод делегата. Делегат получает этот вызов и выполняет необходимые действия.
Наши студенты знакомятся с делегатами, когда им ставится задача реализовать загрузку элемента во вью-контроллере с помощью внешнего сервиса. Нужны следующие шаги:
Добавить новый сервис, который будет получать новые данные.
Вью-контроллер сохраняет ссылку на сервис, чтобы просить его загрузить новые данные.
Сервис возвращает данные через делегирование действий. Для этого ему необходимо связаться с вью-контроллером по протоколу, то есть иметь ссылку на делегат.
ViewController
необязательно должен быть делегатом для выполнения делегированных работ. Можно использовать и другие объекты, однако в этой задаче обработать элемент должен былViewController
.
final сlass MyViewController: UIViewController {
var service: LoadItemService?
func viewDidLoad() {
super.viewDidLoad()
let service = LoadItemServiceImplementation()
service.delegate = self // 1. Создаём связь с делегатом, в качестве делегата — контроллер
self.service = service // 2. Сохраняем ссылку на сервис
}
func viewDidAppear() {
super.viewDidAppear()
service.loadElement()
}
}
protocol LoadElementDelegate {
/// Действие по обработке полученного элемента
func handleLoaded(_ item: VeryImportantItem)
}
extension MyViewController: LoadElementDelegate {
func handleLoaded(_ item: VeryImportantItem) {
// Делаем что-то очень важное с элементом
}
}
final class LoadItemServiceImplementation: LoadItemService {
var delegate: LoadElementDelegate?
func loadElement() {
let item = VeryImportantItem()
delegate.handleLoaded(item) // Делегируем обработку полученного элемента
}
}
Тут новичков поджидает такая ошибка: создание сильной связи между двумя ссылочными объектами.
Retain cycle, или сильный ссылочный цикл — причина утечки памяти в iOS, когда два объекта держат друг друга и не могут освободиться из памяти. Он может возникнуть, когда у двух объектов есть сильные ссылки друг на друга.
В данном случае retain cycle возникает из-за установки сильной ссылки между вью-контроллером и объектом сервиса, а также сохранения реализации сервиса в переменной service
в классе вью-контроллера.
Когда мы устанавливаем delegate
в self
, создаём сильную ссылку между вью-контроллером и сервисом. А когда сохраняем реализацию сервиса в переменную service
в классе вью-контроллера, создаём вторую сильную ссылку, и в результате образуется ссылочный цикл.
Для освобождения памяти в iOS используется система подсчёта сильных ссылок на каждый объект, называемая ARC (Automatic Reference Counting). Она определяет, нужно ли ещё держать объект в памяти. Когда счётчик ссылок достигнет нуля, объект автоматически освободится из памяти.
Чтобы исправить ошибку со ссылочным циклом в данном случае, необходимо ослабить одну из сильных ссылок, используя ключевое слово weak
. В частности, мы можем сделать делегат слабой ссылкой, используя weak var delegate: LoadElementDelegate?
. Это ослабит ссылку между вью-контроллером и сервисом и предотвратит возникновение сильного ссылочного цикла.
weak var delegate: LoadElementDelegate?
Важный момент: ссылка на делегат всегда слабая, а обратная ссылка — сильная.
Здесь необходимо понять принцип паттерна, владеть знаниями о reference type и об устройстве памяти в iOS. Это несложно, особенно когда приложение одностраничное, там эта ошибка вряд ли приведёт к проблемам. Самое интересное происходит, когда начинающий разработчик собирает приложение из нескольких экранов, а ещё лучше, если эти экраны будут создаваться несколько раз.
Рассмотрим другую ошибку с проектированием связей между элементами. Предположим, новая задача состоит из трёх экранов:
Вспомогательный экран
SplashViewController
— который при необходимости предложит пользователю пройти авторизацию, иначе покажет главный экран.За авторизацию будет отвечать
AuthViewController
, о результатах он будет сообщать с помощью делегата на вызвавший егоSplashViewController
.В качестве главного экрана — профиль пользователя
ProfileViewController
. С которого пользователь может разлогиниться.
И пусть первый вариант кода у студента будет приблизительно таким:
final class SplashViewController: UIViewController {
func showAuthVC() {
let authVC = AuthViewController()
authVC.delegate = self
present(authVC, animated: true)
}
func showProfile() {
let profileVC = ProfileViewController()
present(profileVC, animated: true)
}
func loginSuccessful() {
showProfile()
}
}
final class AuthViewController: UIViewController {
var delegate: AuthViewControllerDeleate?
func handleSuccessResult() {
delegate?.loginSuccessful()
}
}
final class ProfileViewController: UIViewController {
func logout() {
let authVC = AuthViewController()
// для упрощения примера пусть эта функция устанавливает
// новый вью-контроллер в иерархии в качестве корневого
replaceCurrentRootVC(with: authVC)
}
}
Здесь мы легко находим ту же самую ошибку, что и в прошлый раз, — ослабляем ссылку на вью-контроллер:
weak var delegate: AuthViewControllerDeleate?
Может показаться, что никто не держит AuthViewController
, но это не так. Контроллер, размещённый в иерархии экранов, уже сильно удерживается. Поэтому нам нужно ослаблять ссылку тоже.
Интереснее следующее: в сценарии logout
повторно создаётся AuthViewController
и размещается в представлении. Здесь новичок забыл установить ему delegate
. Кто тогда будет обрабатывать результат авторизации? В этом случае пользователь попадёт в тупик, потому что никто не обработает действия от AuthViewController
и некому определять, куда навигировать дальше. Здесь мы сталкиваемся с неправильным проектированием связей между элементами.
Здесь студент мог бы реализовать ещё один делегат AuthViewController
и повторить некоторые действия после авторизации. Однако правильнее эту ошибку исправить так: заменить переход не на AuthViewController
, а на вспомогательный контроллер-координатор SplashViewController
, чтобы иметь возможность повторить сценарий входа в приложение и переиспользовать уже написанный код. Ответственностью SplashViewController
будет установить делегата и обработать делегируемые методы.
Эту ошибку я связываю с неловкостью в обращении с этим паттерном. Если сразу вспомнить, что AuthViewController
не будет работать без компаньона-делегата, то такой ошибки бы не возникло.
На самом деле такое случается и в более тривиальном случае: разработчики могут забыть установить делегат таблице и долго дебажить, почему их код не работает как нужно.
Будьте внимательны:
избегайте создания сильной связи между объектами,
не забывайте устанавливать делегат и проверяйте осмысленность созданных связей.
Мы разобрали ошибки, которые я встречаю у новичков чаще всего. Если у вас появились вопросы по поводу ошибок, которые мы разобрали, или если есть другие решения, я буду рада обсудить их в комментариях и помочь вам :)
Что ещё поможет разобраться в iOS-разработке:
iOS Safe Area — подробная статья про использование Safe Area.
Using Guards in Swift to Avoid the Pyramid of Doom — хорошая статья с примерами того, какой код стоит переписывать через guard.
The power of switch statements in Swift — короткая статья с интересными фишками switch.
Пример реализации паттерна delegate — разобрано на примере с протоколами и замыканиями.
Справочник паттернов программирования — есть примеры на Swift, перевод сайта на английский и русский.
Отличная книга Advanced Swift (есть на русском и английском) — позволяет по-новому взглянуть на свой код.