5 ключевых ошибок начинающего iOS-девелопера

Как их избежать.

Никита Хомицевич, Senior iOS Engineer в Life360 и лектор курса «iOS: разработка приложений с 0», рассказывает об основных сложностях на старте iOS-разработки и о том, как с ними справиться.

Я разделил ошибки начинающих iOS-девелоперов на две группы — относящиеся к hard и soft skills. Первые часто связаны с тем, что девелопер не разобрался в особенностях iOS или не изучил подходы к написанию кода (SOLID или DRY). Вторые — со страхом плохо проявить себя в новой команде.

Объясняю, почему я выбрал эти ошибки и как их исправить.

Hard Skills
 

Ошибка #1. Не добавили модификатор weak при использовании паттерна делегирования

Эта ошибка — самая распространенная на старте, но ее проще всего исправить. Посмотрим на пример:

Если ревьюить код, не будучи предельно внимательным к деталям, можно пропустить отсутствие модификатора weak в строке объявления делегата в классе UserDetialsViewController. На что это повлияет?

Представим приложение, состоящее из одного экрана, со списком пользователей в виде таблицы. По каждому пользователю есть данные: имя, фамилия, пол, возраст, вес. В таблице мы видим данные по каждому пользователю, можем нажать на любого из них и перейти на следующий экран, чтобы отредактировать информацию. Давайте представим, что условный пользователь-администратор за одну сессию в приложении может десятки раз заходить на разных пользователей и менять их данные. Теперь вернемся к коду и посмотрим, что произойдет, если мы забудем объявить объект делегата с модификатором weak.

Для этого обратимся к документации Apple, где говорится о strong reference cycle. Допустим, первый экран у нас представлен классом `UsersListViewController` и у него есть свойство `var currentUserDetailsViewController`, где также нет модификатора `weak` или `unowned`. Значит, объект `currentUserDetailsViewController`, который держит вышеупомянутый класс, объявлен как strong reference (то есть, объект по этой ссылке гарантированно существует, и в runtime он не будет удален до тех пор, пока его ссылка будет активна). То есть, по правилам ARC (Automatic Reference Counting) при создании объекта на него будет выделена память, а счетчик ссылок для этого объекта увеличится на единицу. 

В классе `UserDetialsViewController` делегат представлен как strong-ссылка на объект. Логично предположить, что где-то при создании класса `UserDetialsViewController` мы объявили его делегатом объект `self` в рамках класса `UsersListViewController`. Выходит, что класс `UsersListViewController` держит strong-ссылку на класс `UserDetialsViewController`, а `UserDetialsViewController` держит strong-ссылку на `UsersListViewController`. Это приведет к тому, что когда пользователь после запуска приложения впервые откроет детали по какому-то пользователю а затем выйдет назад, экземпляр класса `UserDetialsViewController` останется в памяти и создаст утечку. Если пользователь в течение одной сессии работы приложения перейдет на 10 пользователей, мы соберем столько же утечек памяти по объектам класса `UserDetialsViewController`. Тысячи таких объектов приведут к падению приложения. Тогда вся работа в бэкграунде будет остановлена, и мы потеряем часть данных (они не успеют синхронизироваться с сервером).

Ошибка #2. Использование одного Storyboard для хранения всех экранов в приложении

Нормально иметь в больших и средних приложениях Storyboard с несколькими экранами. Так мы группируем пользовательские сценарии в одном месте. Это удобно для поддержки кодовой базы.

Проблемы появляются, когда разработчик пытается добавить очередной экран в storyboard, в котором уже есть более 2-3 экранов приложения разной сложности. Каждый раз при нажатии на значок storyboard Xcode обрабатывает xml-код выбранного файла и строит заданный интерфейс. И хотя он может кешировать некоторые открытые в прошлом storyboards, часто этот кеш может удалиться. Чем более сложные иерархии у каких-то экранов будут в вашей storyboard и чем больше экранов будет в storyboard в целом, тем дольше вы будете ждать перед монитором, когда Xcode сможет прогрузить и отрисовать все. Разработчики Apple уже давно предоставили набор инструментов, которые помогают при создании интерфейсов через Interface Builder.

Перейдем к решению проблемы.

Во-первых, вместо создания экрана со сложными многоуровневыми компонентами, мы можем вынести их как отдельные сущности, наследуемые от класса `UIView`. Это упростит структуру экрана в storyboard и дальнейшую поддержку кода. Легче поддерживать сложный интерфейс в виде небольших атомарных блоков и отдельных компонентов, чем огромную иерархию таких элементов в одном месте.

Если же экран настолько сложный с точки зрения интерфейса, что нам проще оперировать классами, наследуемыми от `UIViewController`, можно скомпоновать экран из нескольких объектов-наследников `UIViewController`. Они будут выступать в качестве child view controllers

Родительский класс будет называться view controller container и управлять жизненным циклом «детей» в рамках своего жизненного цикла. Этот инструмент дает нам гибкий механизм для разбиения и композиции отдельных блоков при построении интерфейсов со сложной иерархией UI-элементов.

Если пользовательский сценарий обязывает иметь 10-15 экранов (что часто встречается во многих приложениях, где есть длинный этап онбординга пользователя),я рекомендую разбивать один storyboard на несколько файлов. К примеру, если после регистрации для пользователя запланирован длинный онбординг из 5-7 экранов, экран регистрации можно вынести в storyboard к экрану логина. 

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

Чтобы переход с регистрации вел на нужный нам экран знакомства с приложением, в нужно использовать инструмент Storyboard Reference, который есть в Xcode Interface Builder. Его применение подробно описано в статье Refactoring with Storyboard References.

Ошибка #3. Нарушение принципов SOLID

Знание этих принципов не гарантирует, что разработчик понимает, где нужно применять SRP (single responsibility principle) или DIP (dependency inversion principle). Поэтому на собеседованиях девелоперов всех уровней часто спрашивают про принципы SOLID или просят исправить код, где они нарушены.

Разберем такой пример кода с ошибками и узнаем, как должен отвечать кандидат.

Выше — класс `UserDetailsViewController`, в методе `viewDidLoad()` которого идет процесс настройки внешнего вида экрана. Устанавливаются значения header и footer для компонента списка `scrollableMenuView`, вызываются методы `addDefaultSubviews()` для добавления компонентов экрана и `setupDefaultConstraints()` для задания расположения уже добавленных компонентов. 

После вызова строки `super.viewDidLoad()` мы конфигурируем параметры экземпляра класса `UINavigationController` который, скорее всего, является контейнером для текущего класса. 

Мы надеемся,что класс `UserDetailsViewController` будет показан пользователю в стеке навигации через метод `pushViewController(_ viewController:, animated:)`. Но есть нюанс: мы точно не знаем как написана логика навигации. Вероятно, экран будет показываться модально, с помощью метода `present(viewController:, animated:)`. Тогда у сущности этого класса свойство `navigationController` будет `nil`, а значит, код настройки `navigationController` не имел смысла. 

Как гарантировать, что мы правильно произведем навигацию и настроим объект `navigationController`? D в SOLID — это dependency inversion principle, который гласит, что модули верхнего уровня не должны зависеть от модулей нижнего уровня. В примере кода мы нарушили этот принцип — `viewController` знает про детали реализации `navigationController`, хотя логика класса не отвечает за презентацию самого экрана и мы не можем гарантировать корректность работы кода. `navigationController` здесь — сущность более высокого уровня абстракции, чем сущность класса `UserDetailsViewController`, потому что мы хотим, чтобы этот экземпляр находился в навигационном стеке и презентовался через метод `pushViewController(_ viewController:, animated:)`. 

Для этого код конфигурации `navigationController` нужно полностью вынести из метода `viewDidLoad()` нашего класса. Этот код может, например, находиться в месте вызова метода `pushViewController(_ viewController:, animated:)`. Есть разные подходы к организации кода и паттерны проектирования. К примеру, используя архитектуру MVVM, мы можем вынести навигацию в отдельную сущность Coordinator. Одной из ее ответственностей будет настройка `navigationController` перед показом нужного экрана.

Soft Skills
 

Ошибка #1. Боязнь рассказать о проблеме

Когда я был junior-разработчиком, нам с напарником (senior-ом) дали проект, где нужно было разработать iOS- и Android-приложения для физического устройства, считывающего показатель CO2. С помощью приложения можно было подключаться к устройству, управлять им, получать данные и манипулировать ими. Проект состоял из двух фаз: в первой фазе мы доделывали функционал библиотеки, в которой хранилась логика работы с данными, а также логика установки соединения с устройством. После, если заказчика бы все устроило, проект продлили бы на вторую фазу, куда входила бы и работа над основным приложением.

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

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

Когда я разобрался в кодовой базе, стало ясно, что реализация работает неправильно, и нужно написать много кода для синхронизации данных при загрузке прошивки и последующей перепроверки всех датчиков между клиентом и устройством. Это требовало использования GCD (Grand Central Dispatch). К примеру, мне понадобилось использовать `dispatchAsync(with .barrirer)`, чтобы синхронизировать некоторые участки кода в одном месте и `DispatchGroup` для получения необходимых данных (после проверки всех сенсоров) подтверждающих, что устройство работает корректно. Тогда я не знал про асинхронные запросы с барьером и `DispatchGroup`. Подойти и попросить помощи мне не хватило смелости. Суть задачи не была мне ясна до конца, искал я не то что нужно. Оставалось всего 2 недели до сдачи проекта, и я рассказал о проблеме только в конце недели. Опыт моего коллеги позволял без особых проблем придумать правильный алгоритм работы синхронизации данных. В пятницу вечером он забрал у меня задачу и уже к среде необходимый код был написан. К пятнице мы успели со сроками, но напарнику пришлось работать три дня по 10 часов, чтобы тестировщики успели проверить новую функциональность.

Если бы я сразу пошел к напарнику, ему не пришлось бы перерабатывать. А я бы чувствовал себя как ответственный работник, который может найти решение.

Ошибка #2. Страх проявить инициативу

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

Представим себе коллектив из 4 iOS-разработчиков. К ним вышел пятый разработчик — junior iOS. Тимлид пытается быстро интегрировать его во все процессы в команде. Он устраивает сессии парного программирования, ставит задачи и проверяет, все ли в порядке. Он также привлекает других членов команды, пытаясь социализировать новичка. Через некоторые время тимлид дает джуну полноценные рабочие задачи с минимум контроля. Так он пытается показать, что новичок уже набрался базового опыта, и ему можно доверять. 

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

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

Если это новый экран, и есть вопросы по UI — нужно идти к дизайнеру. Если это экран для редактирования персональных данных пользователя, важно понимать цифры, то есть, знать аналитику по событиям изменений, которые совершает каждый конкретный пользователь. В случае, когда ты не видишь в описании аналитики, можешь подойти к product manager и уточнить, не забыл ли он о ней. 

Смотри на задачи шире, и если кажется, что чего-то не хватает, — действуй.

Ещё
Как машинное обучение помогает создавать лекарства.