Привет! Я Александра Башкирова, iOS-инженер в Clover и старший код-ревьюер на курсе «iOS-разработчик» в Яндекс Практикуме. На момент подготовки статьи мы уже проверили более тысячи студенческих работ и успели заметить повторяющиеся ошибки.
В этой статье я разберу несколько популярных ошибок начинающих iOS-разработчиков, чтобы вы могли не повторять их в своей работе. Мы вместе посмотрим, к чему они приводят и как их можно избежать. Начнём с вёрстки, а потом посмотрим на код и выработаем стратегии для хороших практик.

Часть 1. Вёрстка
Мы учим студентов верстать в В примере я переименовала Получается, что Storyboard не узнал о том, что мы поменяли название ссылки на элемент. Чтобы решить эту проблему, нужно привести название к одинаковому состоянию в коде и в Storyboard. Для этого можно удалить старую связь с аутлетом в Connection Inspector и создать новый аутлет либо поменять название в коде на то, которое понимает ваш Storyboard. Когда вы меняете название аутлета или экшена в коде, нужно гарантировать, что это же название поменялось и в вашем Storyboard. К счастью, таких проблем можно избежать, если для изменения названий пользоваться встроенным в Xcode рефакторингом Refactor → Rename — тут Xcode подскажет, что нужно обновить имя ссылки в сториборде тоже.
helloLabel в labelhelloLabel — это название элемента в момент, когда мы создавали связь со сторибордом. Дело в том, что в сториборде эта связь осталась неизменной, хотя во вью-контроллере тот же элемент уже называется label. Storyboard вполне текстовый документ, его можно открыть в виде XML-документа и найти лейбл, описанный в таком блоке:<connections> <outlet property="helloLabel" destination="jeX-w7-wVM" id="hGw-e6-vqM"/> </connections> 
Ошибки нас ждут также, если удалим в Connection Inspector связь у Storyboard с кодом, но продолжим обращаться к оставшимся аутлетам из кода. @IBOutlet создаётся неявным опционалом, потому что сама инициализация элемента не может быть сделана в момент создания контроллера. Инициализация будет выполнена системой позже, а если такой связи на сториборде нет, то не будет выполнена никогда.
Попробуем обновить значение в helloLabel, но удалим связь в Storyboard: как только приложение дойдёт до строчки с установкой значения — произойдёт краш в runtime. Мы попробовали установить свойство text несуществующему объекту, и такое обращение было небезопасным, ведь свойство было неявным опционалом.
Если приложение упало, там будет подсказка, в чём причина
В этом примере Xcode подсказывает пустым кружочком рядом с аутлетом, что связи с элементом нет. Если мы хотим поменять значение лейбла на Хабр, привет!, то нужно будет восстановить ссылку на него, добавив связь заново. Например, соединив кружочки напротив new referencing outlet со свойством helloLabel.
Итак, если вы столкнулись с runtime-ошибкой, которая упоминает ваш аутлет или экшен, обязательно проверьте:
-
Совпадение названий элемента или функции в коде и в сториборде.
-
Наличие связи с кодом у элемента на вкладке Connection Inspector.
Использование Auto Layout
Пожалуй, самая типичная ошибка новичка — создать красивый интерфейс для новенького iPhone Pro Max, но не адаптировать его под размеры других девайсов.

Адаптивный интерфейс верстаем с помощью 
Важное свойство системы неравенств: она может иметь несколько решений. Именно поэтому возникают ошибки и предупреждения в Interface Builder, а также предложение «Добавить недостающие констрейнты». Из-за этого можно получить совершенно неожиданный результат: предполагаешь одно конкретное решение, а в ходе расчёта Auto Layout может подобрать совершенно другое решение неравенств 🙂

Изучив основы Auto Layout, можно переходить к работе с Например, на скриншотах выше можно задать серой 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 могут быть различными в зависимости от текущей var lastPage: Int? = nil lastPage! += 1 // Произойдёт ошибка: операция на nil не может быть выполнена
Swift — типобезопасный язык. В нём достаточно красивых способов написать аккуратный код без использования force unwrap. В сообществе iOS-разработчиков принято писать безопасный код и избегать force-операций, ведь они могут привести к сбою приложения, и оно будет вылетать. Пока вы изучаете разработку под iOS, будьте бдительны в следующих ситуациях: Когда Xcode предлагает «исправить» код на операции с восклицательным знаком: это и есть force-операции, их стоит избегать и переписывать на безопасные конструкции. Присмотритесь к различным туториалам — там часто встречается код с force unwrap, но в учебных материалах force unwrap удобен именно для упрощения объяснения концепции и быстрого старта. Вы можете отмахнуться от этого совета: «Ничего страшного не произойдёт в моём пет-проекте!» Да, не произойдёт. Относитесь к этому не только как совету писать безопасный код, но ещё и как к формированию best practice. В тестовых заданиях и на coding-интервью работодатели особенно пристально следят за форсами и могут уже на этом этапе забраковать вашу кандидатуру. Лучше сразу привыкать писать код так, как принято в сообществе. Итак, если вы разобрались, что обязательно нужно проверить, безопасны ли операции на опционалах, вы можете написать что-то подобное: Это действительно безопасный вариант кода, но это тот случай, когда без него можно обойтись. Мы рекомендуем тренировать привычку использовать возможности языка: Optional Binding — самый используемый способ, когда мы распаковываем элемент, тем самым сразу проверяя на опциональность: Изначальный пример мы можем написать так: Nil Coalescing Operator — позволяет взять дефолтное значение в случае опциональности: Снова попробуем переписать наш изначальный пример: Guard Statement — позволяет прервать выполнение блока, если значение опционально. Если вы работаете с опциональным значением, сначала попробуйте подобрать подходящую стратегию. Нужно ли в этом случае выполнять какой-то код, или вы можете заменить его предзаданным значением? Стоит ли показать ошибку? Смело подбирайте подходящую конструкцию, в большинстве случаев ваш код можно написать, используя их. Force unwrap можно использовать в редких ситуациях, если вы на 100% уверены, что переменная никогда не будет зависеть от внешнего окружения и не превратится в optional. Например, константа с указанием адреса: Здесь мы, во-первых, уверены, что описали верную ссылку на сайт, во-вторых, это константа, и её значение статично независимо от состояния приложения. Однако если мы немного изменим этот код, используя внешний аргумент, то гарантировать то же самое нельзя. В таком случае стоит переписать код на безопасный вариант разворачивания опционала: guard Этот оператор — мой любимый в Swift. Конструкция нужна для проверки условий и распаковки опциональных значений с использованием выхода из функции или блока, если условия не выполнились. Этот оператор удобно использовать, чтобы распрямить код, то есть сделать его менее вложенным. Однако новички любят условный оператор if и используют его очень активно: Получился вполне безопасный вариант, ведь аргумент Разработчики не любят глубокую вложенность, потому что такой код сложнее тестировать, читать и в нём легче ошибиться. Поэтому всячески стараются распрямлять код — например, с помощью С помощью Итак: используйте Иногда Мы уже посмотрели, как можно избегать force unwrap, и в данном случае с Этот пример кода имеет и архитектурную проблему: функция принимает два параметра, хотя предполагается, что только один из них будет неопциональным. Возможны ситуации с другими вариантами: Используйте Помещайте Используйте Используйте блок Следите за размером блока Используйте switch Этот оператор можно использовать для проверки значений различных типов данных, таких как числа, строки, перечисления (enum), булевы значения и другие. Новичок использует Рассмотрим ещё пару особенностей использования В блоке Использование варианта На что стоит обратить внимание при работе со Использование оператора не должно быть избыточно, и если его легко можно переписать с помощью Перечислять все возможные случаи, чтобы убедиться, что ничего не упущено, и иметь возможность заметить ошибку при дополнении новыми случаями. Использовать блок Начинающим разработчикам непросто даётся понимание делегирования, хотя это самый часто используемый 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 возникает из-за установки сильной ссылки между вью-контроллером и объектом сервиса, а также сохранения реализации сервиса в переменной Когда мы устанавливаем Для освобождения памяти в iOS используется система подсчёта сильных ссылок на каждый объект, называемая ARC (Automatic Reference Counting). Она определяет, нужно ли ещё держать объект в памяти. Когда счётчик ссылок достигнет нуля, объект автоматически освободится из памяти. Чтобы исправить ошибку со ссылочным циклом в данном случае, необходимо ослабить одну из сильных ссылок, используя ключевое слово Важный момент: ссылка на делегат всегда слабая, а обратная ссылка — сильная. Здесь необходимо понять принцип паттерна, владеть знаниями о reference type и об устройстве памяти в iOS. Это несложно, особенно когда приложение одностраничное, там эта ошибка вряд ли приведёт к проблемам. Самое интересное происходит, когда начинающий разработчик собирает приложение из нескольких экранов, а ещё лучше, если эти экраны будут создаваться несколько раз. Рассмотрим другую ошибку с проектированием связей между элементами. Предположим, новая задача состоит из трёх экранов: Вспомогательный экран За авторизацию будет отвечать В качестве главного экрана — профиль пользователя И пусть первый вариант кода у студента будет приблизительно таким: Здесь мы легко находим ту же самую ошибку, что и в прошлый раз, — ослабляем ссылку на вью-контроллер: Может показаться, что никто не держит Интереснее следующее: в сценарии Здесь студент мог бы реализовать ещё один делегат Эту ошибку я связываю с неловкостью в обращении с этим паттерном. Если сразу вспомнить, что На самом деле такое случается и в более тривиальном случае: разработчики могут забыть установить делегат таблице и долго дебажить, почему их код не работает как нужно. Будьте внимательны: избегайте создания сильной связи между объектами, не забывайте устанавливать делегат и проверяйте осмысленность созданных связей. Мы разобрали ошибки, которые я встречаю у новичков чаще всего. Если у вас появились вопросы по поводу ошибок, которые мы разобрали, или если есть другие решения, я буду рада обсудить их в комментариях и помочь вам 🙂 Что ещё поможет разобраться в 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 (есть на русском и английском) — позволяет по-новому взглянуть на свой код.
var lastPage: Int? // Заведём некоторую опциональную переменную // Проверим её на nil и в тернарном операторе // возьмём ноль, если значение nil, // или прибавим единицу, если значение существует let nextPage = lastPage == nil ? 0 : lastPage! + 1
// Объявим опциональную переменную. Если не присвоить в неё значение, то по умолчанию будет присвоен 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 // использовали начальное значение }
let value = optionalValue ?? defaultValue var lastPage: Int? // Возьмём значение, если оно есть, и прибавим единицу, чтобы получить следующую страницу // Возьмём -1 в случае nil, прибавим единицу — получим индекс первой страницы, 0 let nextPage = (lastPage ?? -1) + 1
guard let lastPage = lastPage else { return } // используйте lastPage здесь guard let currentPage else { // Короткий вариант в Swift 5.7, аналогичный if let return } // используйте currentPage здесь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, всё взорвалось! 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 } // делаем что-то ещё }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 включает в себя блоки 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
service в классе вью-контроллера.delegate в self, создаём сильную ссылку между вью-контроллером и сервисом. А когда сохраняем реализацию сервиса в переменную service в классе вью-контроллера, создаём вторую сильную ссылку, и в результате образуется ссылочный цикл.weak. В частности, мы можем сделать делегат слабой ссылкой, используя weak var delegate: LoadElementDelegate?. Это ослабит ссылку между вью-контроллером и сервисом и предотвратит возникновение сильного ссылочного цикла. weak var delegate: LoadElementDelegate?
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 не будет работать без компаньона-делегата, то такой ошибки бы не возникло.
ссылка на оригинал статьи https://habr.com/ru/company/yandex_praktikum/blog/722812/
Добавить комментарий