Мы разобрались, как избежать рутины с помощью шорткатов для навигации, редактирования и отладки, чем могут помочь кастомные сниппеты и как прокачать файловые шаблоны. Описали это в «методичке» по командам в Xcode. Добавляйте в закладки, чтобы посмотреть при случае (а навигация в статье поможет быстро найти то, что нужно).



Оглавление


Шорткаты и хоткеи
Навигация
Редактирование
Отладка
Кастомизация
Сниппеты
Шаблоны
Бонус фича

Шорткат или хоткей?


Шорткат — то, что дает быстрый доступ к пункту меню (копировать/вставить/удалить).

Хоткей — то, у чего нет аналога в пункте меню/интерфейсе взаимодействия (кастомный биндинг).

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

Примерно полтора года назад мы стали практиковать изучение шорткатов, хоткеев, сниппетов и файловых шаблонов на уровне iOS-команды, чтобы избавиться от рутины. Например, распечатывали листы с расширенными командами (базовые уже знали) и держали их под рукой, чтобы учить. Это вошло в практику и теперь мы тренируем команды во время онбординга (и порой сами узнаем что-то новое).

Использование шорткатов трудно переоценить: попробуйте вспомнить, когда вы последний раз копировали что-либо через контекстное меню (ПКМ -> Копировать). Эти операции делаются на автомате. Давайте рассмотрим, какие еще операции мы можем довести до автоматизма при работе в Xcode.

В этой статье рассмотрим шорткаты для UIKit-flow. 

Базовые клавиши для активации шорткатов:

  • ? — command;
  • ? — option;
  • ? — control;
  • ? — shift.

Базовые шорткаты:

  • ? + B — build;
  • ? + R — run;
  • ? + U — run unit tests;
  • ? +. — stop build/run/run unit tests;
  • ? + ? + ? + K — clean build folder.

Уверен, большинство из вас знакомы с ними, поэтому перейдём к интересному. Шорткаты разбиты на три блока: навигация, редактирование и отладка.

Навигация


Начнём автоматизацию рутины с шорткатов для навигации:

? + 0 (ноль) — показать/скрыть панель Navigator:



? + цифры — быстрое перемещение по вкладкам панели Navigator:



Повторим трюк с панели навигации для панели Inspectors и научимся скрывать/показывать её. Просто добавляем option: ? + ? + 0 (ноль):



С зажатым option мы также можем пройтись по всем вкладкам панели — ? + ? +цифры:



Assistant Editor скрыть не получиться, зато в нём можно делать много крутых штук.

? + ? + O — быстро найдем и откроем файл. Открыть можно не только файлы проекта, но и файлы с документацией iOS:



Если найденный файл хочется открыть рядом, зажимаем ?, перед тем, как нажать Enter:



Либо, можно зажать ? + ? — в этому случае мы сами выбираем, где необходимо расположить радактор:



Перемещаться между редакторами можно с помощью сочетания ? + J + стрелочки для выбора:



  • ? + ? + T — позволяет открыть несколько редакторов (зажмите столько, сколько нужно);
  • ? + ? + ? + T — меняет ориентацию для открываемого редактора (если предыдущий редактор открывался горизонтально, то новый откроется вертикально, и наоборот);
  • ? + ? + J — поможет быстро найти файл в структуре проекта.



  • ? + T — добавить новую вкладку;
  • ? + W — закрыть текущую вкладку;
  • ? + ? + \ — посмотреть все вкладки.



Рассмотрим навигацию внутри панели, отображающую путь до файла:



Все взаимодействия с ней происходят путём зажатия ? и цифры.

Для всех сочетаний данной панели (за исключением ? + 1) вы можете использовать поиск: после того как сработал хоткей, начните писать название файла/класса. Так работать с данной панелью гораздо удобнее.

? + 1 — позволяет узнать о суперклассах нашего класса, какие протоколы он поддерживает, кто вызывает его методы, какие экстеншены есть у класса.


  • ? + 2 и ? + 3 — отображает список файлов, по которым мы перемещались. Эти сочетания отлично дополняют два других: ? + ? + стрелочки влево/вправо для непосредственной навигации по истории просмотренных файлов назад и вперёд;
  • ? + 4 — открывает окно для навигации по структуре проекта (мало чем полезно, на мой взгляд, часто проекты имеют большую вложенность);
  • ? + 5 — навигация по файлам внутри директории, в которой открыт текущий файл;
  • ? + 6 — навигация по свойствам и методам класса в текущем файле.

Идем дальше:

  • ? + L — переход к строке кода;
  • ? + курсор на миникарте — показать структуру файла.



? + ? + ? + Enter — открыть класс для сториборда в отдельном редакторе:



? + ?+ ? + ? или ? + ? + ? + ПКМ — контекстное меню иерархии вьюх в сториборде:



Редактирование


  • ? + стрелки влево/вправо — позволяют быстро перейти в начало/конец строки;
  • ? + стрелки влево/вправо — быстрое перемещение между словами без пробелов;
  • Control + стрелки влево/вправо — перемещение между словами в camelCase

Здесь есть хитрость: изначально эта комбинация в macOS переключает рабочие столы, поэтому для работы хоткея в Xcode, его нужно переопределить, либо изменить в System Preferences.


Вот что мы сможем делать в итоге:



Все эти сочетания отлично работают с зажатым Shift для выделения. А вот что дальше можно делать с этим выделенным блоком:

? + ?+ [ или ] — перемещает выделенный блок/строчку вверх/вниз. Самая крутая фича здесь — автоматический заход в скоупы.



  • ? + [ или ] — перемещает выделенный блок (строку) влево/вправо;
  • ? + I — автоматическое выравнивание выделенного блока (согласно настройкам в Xcode -> Preferences -> Text Editing -> Indentations);
  • ? + выделение курсором — такая комбинация создает множественные курсоры. Удобно проставлять атрибут доступа в модельке или добавлять/удалять одинаковые части.



  • ? + / — закомментировать/раскомментировать текущую строку;
  • ? + ? + E — редактировать выделенную сущность (но не забудьте поправить конструктор, его шорткат не затрагивает).



  • ? + F — поиск по файлу;
  • ? + ? + F — поиск по проекту;
  • ? + ? + F — поиск и замена;
  • ? + ? + ? + F — фикс всех ошибок в скоупе (не совсем то, о чем вы подумали, но  тоже очень крутая вещь). В данном примере явно указывается модификатор доступа public.



? + ? + A — показать контекстное меню действий. В зависимости от того, для кого мы вызываем это меню, Xcode предложит релевантные действия. В списке также можно использовать поиск: 



Отладка


Я заметил, что в нашей команде мало кто пользуется шорткатами для консоли, а ведь это не самое редкое место в работе. Давайте изучать.

Брейкпойнты:

  • ? + \ — добавить/удалить брейкпойнт;
  • ? + Y — отключить (disable) брейкпойнт.

Консоль:

  • ? + ? + c — открыть консоль;
  • ? + ? + Y — скрыть консоль.

Навигация внутри консоли:

  • ? + K — очистить содержимое консоли;
  • s — step into (step);
  • n — step over (next);
  • finish — step out;
  • c — continue;
  • breakpoint disable — отключить все брейкпойнты.

В итоге работа с дебагером будет выглядеть примерно вот так:



Кастомизация


В настройках Xcode можно задать свои биндинги для операций. Например, зададим хоткей для оборачивания строки в NSLocalizedString. Для этого перейдем во вкладку Key Bindings настроек Xcode и найдем интересующую команду:



У себя в команда мы решили что это будет сочетание ^ + ? + L. Можем смело использовать:



Или добавить хоткей для удаления строки целиком — ? + D (? + backspace удаляет только текст):



Возможно не самое очевидное сочетание для такой операции, но использовать удобно:



Можно не только задавать, но и изменять существующие биндинги. Например, зачем мне шорткат для печати в Xcode? Удалил его, теперь можно забиндить на него что-то полезное.

Xcode позволяет создать несколько биндинг сетов. Можно создать сет и пошарить его в команде, это позволит более эффективно работать в паре. Файлы биндингов расположены здесь: ~/Library/Developer/Xcode/UserData/KeyBindings

Наша команда на этом не остановилась — а что если забиндить запуск скриптов, например для установки/переустановки подов? В Xcode, в отличие от AppCode, нет встроенного терминала, приходится переключаться между вкладками. Получается, чтобы установить/переустановить поды в проекте, необходимо переключиться на терминал, перейти в папку с проектом и прописать команду. Ужасно долго. Но решение есть!

Воспользуемся другой функциональностью Xcode — Behaviors:

  1. добавим поведение через + внизу слева;
  2. зададим хоткей для вызова (как раз один освободился);
  3. укажем путь до скрипта.



В нашем случае скрипт выглядит так: запускаем терминал (если он не запущен), переходим в папку с проектом и можем указывать нужные команды для работы.

#!/bin/sh
osascript <<END
tell application "Terminal"
if not (exists window 1) then reopen
activate

do script "cd `pwd`;bundle install" in window 1
do script "cd `pwd`;bundle exec pod install" in window 1
end tell
END

Теперь можно пользоваться терминалом не выходя из Xcode: ? + P запустит скрипт на переустановку. Зажать сочетание — это всё, что от нас нужно.

Какие еще разделы для шорткатов можно прокачать:

  • рефакторинг — extract to function/method/variable, add missing switch case;
  • брейкпойнты — создание Swift Error/Exception/Symbolic/Test Failure;
  • Version Control Source — push/pull/fetch/new branch/checkout;
  • работа с Swift Package Manager — add/reset/resolve/update.

Как изучать шорткаты в программах? Как минимум, все стандартные приложения macOS имеют вкладку Help с возможностью поиска. Поиск в интерактивном режиме не только проведет вас по всем самым потаённым местам, но и подскажет шорткат (сам поиск удобно вызывать хоткеем ? + ? + /).



Сниппеты


Иногда, работая над проектом, я понимал, что раз за разом пишу одно и то же — так называемый бойлерплейт. Этот процесс тоже можно автоматизировать. К счастью, сейчас есть много очень хороших решений и они скорей всего вам знакомы: это и Sourcery для генерации кода по шаблону и библиотеки по типу R.Swift для генерации доступа к ресурсам, и другие фреймворки. Но не всегда есть возможность затащить стороннюю зависимость в проект. К тому же есть бойлерплейт-код, где подобные библиотеки не помогут. Зато поможет стандартный инструмент Xcode — сниппеты.

Сниппеты есть для всех ключевых слов языка Swift (и некоторых других) и стандартных конструкций: циклов, ветвлений, функций. Но часто мне нужен не просто var, а private/public var. Точно также мне не нужен просто @testable, мне нужен @testable import. Другими словами, я бы хотел оперировать уже готовыми конструкциями, а не собирать их по частям.

Изменить стандартные сниппеты нельзя, зато добавить свой совсем просто: выделяем блок кода и в контекстном меню выбираем «Create Code Snippet». Дальше Xcode предложит его настроить: задать имя, добавить описание, платформу, скоуп вызова, и, конечно же, указать комплишн, по которому будет вызываться наш сниппет:



Вот так быстро можно «сгенерировать» код для минимальной работы таблицы (ещё быстрее можно сделать, если сразу в сниппете задать количество строк и возвращать пустую ячейку).

Так выглядит сниппет в библиотеке Xcode (? + ? + L):


Как добавить плейсхолдер: поместите ваш текст внутри конструкции <# MyDescription #> (лучше добавлять её  уже после того, как вы добавили описание, так как она сразу «схлопывается»).

А вот как в два тапа можно сделать импорт целой пачки фреймворков для юнит-тестов:



В проекте «Додо Пиццы» мы пишем тесты с помощью Quick/Nimble. Каждый новый спек начинается с импорта 4–6 фреймворков (минимум), и продолжается телом разного уровня вложенности. Импортировать фреймворки мы уже научились, сделаем это и для тела спеки. Всего пара тапов по клавиатуре и сетап для тестов готов:



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



Для меня это идеальный кандидат для оптимизации своего времени — пусть сниппет делает все за меня:



А сколько еще таких мест можно улучшить (и всё это бесплатно). Несколько личных примеров кандидатов для сниппета:

  • Делегат и data source для таблицы/коллекции (обязательные методы).
  • При объявлении методов протокола срабатывал стандартный сниппет функции (с открытыми фигурными скобками). Но мне не нужны скобки в протоколе, поэтому я добавил свой сниппет, без скобок. Теперь не нарадуюсь.



Часто типы данных и функции нужно объявлять с явным типом доступа. Это можно можно завернуть в сниппет.



  • weak self для замыканий — guard let weakSelf = self else { return };
  • локализация — оборачивание в NSLocalizedString (основное отличие от хоткея описанного выше в том, что здесь мы указываем параметр для бандла).



Шаблоны


Плавно продолжаем тему кодогенерации кода, но теперь поднимемся на уровень выше - шаблоны файлов.

Шаблоны удобны, но Apple не предоставляет инструменты для их разработки и документации. Но можно сделать все ручками, потому что есть доступ к стандартным шаблонам файлов (и проектов) Xcode. Чтобы их найти, перейдём в каталог по этому пути:

// ? + ? + G в Finder

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class

Дальше разместим свои темплейты: создаём папку по пути ниже, её имя будет отображаться в окне Xcode. Я назвал папку Helpers:

~/Library/Developer/Xcode/Templates/Files Templates/Helpers

Шаблон для Xcode — это каталог, куда входят:

  • файл шаблона (например, .swift или .storyboard);
  • .plist файл с настройками;
  • иконки двух размеров для отображения в окне Xcode при создании файла.

Чтобы создать свой шаблон, скопируем папку со стандартным шаблоном Xcode  и изменим его под себя. Например, для создания объекта API реквеста у себя в проекте мы используем определенную структуру, где описываем стандартные для запроса вещи: урл, параметры, заголовки.

Часто нам интересны только пара параметров, но всё остальное мы обязаны прописать (обязанность протокола). Как раз для этого случая я создал шаблон файла, который не только экономит мне время, но и сразу подсказывает где и что дописать.

Вот так может выглядеть .plist файл в нашем случае:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "<a href="http://www.apple.com/DTDs/PropertyList-1.0.dtd">http://www.apple.com/DTDs/PropertyList-1.0.dtd</a>">
<plist version="1.0">
    <dict>
        <key>Platforms</key>
        <array>
            <string>com.apple.platform.iphoneos</string>
        </array>
        <key>Kind</key>
        <string>Xcode.IDEFoundation.TextSubstitutionFileTemplateKind</string>
        <key>Description</key>
        <string>Internet request</string>
        <key>Summary</key>
        <string>Internet request</string>
        <key>SortOrder</key>
        <string>1</string>
        <key>AllowedTypes</key>
        <array>
            <string>public.swift-source</string>
        </array>
        <key>DefaultCompletionName</key>
        <string>File</string>
        <key>MainTemplateFile</key>
        <string>___FILEBASENAME___</string>
        <key>Options</key>
        <array>
            <dict>
                <key>Default</key>
                <string>Some</string>
                <key>Description</key>
                <string>Name of the request</string>
                <key>Identifier</key>
                <string>productName</string>
                <key>Name</key>
                <string>Request name:</string>
                <key>NotPersisted</key>
                <string>Yes</string>
                <key>Required</key>
                <string>Yes</string>
                <key>Type</key>
                <string>text</string>
            </dict>
        </array>
    </dict>
</plist>

Добавил код в .swift файл (полный пример на GitHub) и обновил иконки. Теперь можно протестировать создание в Xcode (возможно потребуется его перезапуск). Появилась отдельная секция Helpers с моим шаблоном.



А вот такое окно мы увидим после выбора шаблона:



Вводим имя для реквеста. В итоге будет создан вот такой файл:



Только посмотрите сколько работы за меня делают шаблоны!

Пара моментов:

  • У нас есть правило для именования реквестов — постфикс Request в имени. При создании файла мне не нужно об этом заботиться, я только ввожу имя реквеста, и по шаблону будет создан класс с правильным названием и именем файла. GetMenuRequest и GetMenuRequest.swift в данном случае.
  • Класс имплементирует протокол, но большинство параметров — дефолтные. После создания файла, шаблон мне «подсказывает» места, которые скорее всего я хочу изменить.

Рассмотрим более интересное применение шаблонов — многофайловые шаблоны с возможностью кастомизации при создании (code variants).

В проекте «Додо Пиццы» мы отделяем вью от котроллера. Если это новый контроллер, то создаем под него сториборд/ксиб. Чтобы создать один экран, нужно создать минимум 3 файла: вью, контроллер и сториборд. Помимо этого, контроллер должен быть связан с вью, а сториборд иметь ссылки на вью и контроллер. При этом, мне не всегда нужно создавать файл сториборда (если он уже есть), или файл с вью (если она не нужна). В этой ситуации помогут многофайловые шаблоны с кастомизацией.

В нашем случае нужно будет создать 4 папки для шаблона:

  1. просто контроллер;
  2. контроллер и сториборд;
  3. контроллер и вью;
  4. контроллер, вью и сториборд;

Вот так будет выглядеть иерархия:

- ViewController.xctemplate
- — UIViewController
- — UIViewControllerStoryboard
- — UIViewControllerView
- — UIViewControllerViewStoryboard
- — TemplateIcon.png
- — TemplateIcon@2x.png
- — TemplateInfo.plist

Здесь уже привычные файлы с иконкой и конфигурацией, но сами шаблоны хранятся в папках. В зависимости от того, какие параметры мы укажем в Xcode при создании контроллера, выберется нужна папка.

Вот так может выглядеть plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "<a href="http://www.apple.com/DTDs/PropertyList-1.0.dtd">http://www.apple.com/DTDs/PropertyList-1.0.dtd</a>">
<plist version="1.0">
    <dict>
        <key>Kind</key>
        <string>Xcode.IDEFoundation.TextSubstitutionFileTemplateKind</string>
        <key>Description</key>
        <string>ViewController class</string>
        <key>Summary</key>
        <string>ViewController class</string>
        <key>SortOrder</key>
        <string>1</string>
        <key>DefaultCompletionName</key>
        <string>ViewController</string>
        <key>Platforms</key>
        <array>
             <string>com.apple.platform.iphoneos</string>
        </array>
        <key>Options</key>
        <array>
             <dict>
                  <key>Identifier</key>
                  <string>viewControllerName</string>
                  <key>Required</key>
                  <true/>
                  <key>Name</key>
                  <string>ViewController:</string>
                  <key>Description</key>
                  <string>The name of the view controller class to create</string>
                  <key>Type</key>
                  <string>text</string>
                  <key>NotPersisted</key>
                  <true/>
                 </dict>
             <dict>
                  <key>Identifier</key>
                  <string>cocoaTouchSubclass</string>
                  <key>Required</key>
                  <string>YES</string>
                  <key>Name</key>
                  <string>Subclass of:</string>
                  <key>Description</key>
                  <string>What class to subclass in the new file</string>
                  <key>Type</key>
                  <string>class</string>
                  <key>Default</key>
                  <string>UIViewController</string>
                  <key>NotPersisted</key>
                  <true/>
                 </dict>
             <dict>
                  <key>Identifier</key>
                  <string>View</string>
                  <key>Name</key>
                  <string>Also create View file</string>
                  <key>Description</key>
                  <string>Whether to create a View file with the same name</string>
                  <key>Type</key>
                  <string>checkbox</string>
                  <key>Default</key>
                  <string>true</string>
                  <key>NotPersisted</key>
                  <true/>
                 </dict>
             <dict>
                  <key>Identifier</key>
                  <string>Storyboard</string>
                  <key>Name</key>
                  <string>Also create Storyboard file</string>
                  <key>Description</key>
                  <string>Whether to create a Storyboard file with the same name</string>
                  <key>Type</key>
                  <string>checkbox</string>
                  <key>Default</key>
                  <string>true</string>
                  <key>NotPersisted</key>
                  <true/>
                 </dict>
             <dict>
                  <key>Default</key>
                  <string>___VARIABLE_viewControllerName___</string>
                  <key>Identifier</key>
                  <string>productName</string>
                  <key>Type</key>
                  <string>static</string>
                 </dict>
        </array>
    </dict>
</plist>

А вот что получим в итоге:



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



Сила шаблонов в том, что на уровне создания файла можно конфигурировать, что именно нужно создать. Например, можно создать шаблон для VIPER-модуля и при создании указывать только то, что вам нужно (презентер с интерактором или весь модуль целиком).

Бонус секция для шаблонов


Как раньше упоминал, для шаблонов нет официальной документации и инструментов для их создания. Возможности шаблонов «перебираются» энтузиастами на основе стандартных шаблонов Xcode. Но есть одна фича, описание которой я не видел ни в одной статье по шаблонам (возможно, эта будет первой).

Фича в том, что в шаблоне мы можем сконфигурировать текстовое поле так, что оно будет выступать в роли превью для вводимого текста. Например, это можно видеть при создании нового проекта — поле Bundle Identifier меняется в зависимости от поля Product Name. В моем примере это выглядит так:



Потрясающе, ведь теперь я знаю, что мне не нужно дописывать бойлерплейт части ViewController и View.

Сделать это просто: в .plist добавим новое текстовое поле, где в default-параметре укажем переменную для интересующего идентификатора.

// Поле ввода имени ViewController
<dict>
      <key>Default</key>
      <string>Massive</string>
      <key>Identifier</key>
      <string>productName</string>
      <key>Required</key>
      <true/>
      <key>Name</key>
      <string>ViewController:</string>
      <key>Description</key>
      <string>The name of the view controller class to create</string>
      <key>Type</key>
      <string>text</string>
      <key>NotPersisted</key>
      <string>YES</string>
</dict>

// Превью имени ViewController
<dict>
      <key>NotPersisted</key>
      <string>Yes</string>
      <key>Default</key><string>___VARIABLE_productName:identifier___ViewController</string>
      <key>Description</key>
      <string>ViewController</string>
      <key>Identifier</key>
      <string>previewViewController</string>
      <key>Name</key>
      <string>ViewController:</string>
      <key>Type</key>
      <string>static</string>
</dict>

Выглядит эффектно. Спасибо @ihppie за то, что рассказал про эту фичу.

Итог


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

  • Тренируйте шорткаты — работа на тачпаде/мышкой будет замедлять вас. Многие операции мышкой и так требуют зажатия клавиш, почему бы полностью не перебраться на клавиатуру?
  • Используйте сниппеты для часто повторяемых, неделимых конструкций, project-specific кейсов.
  • Создавайте шаблоны и управляйте не только создаваемым файлом, но и сопутствующими (code variants).

Код со всеми примерами в репозитории на GitHub.
Делитесь в комментариях своими джедайскими техниками работы с кодом, что используете и в каких количествах, как учили или учите — вместе мы станем на один шаг ближе к 10x инженеру. Чтобы не пропустить следующую статью, подписывайтесь на канал Dodo Pizza Mobile и Dodo Engineering.