Привет, я Саня — iOS-разработчик и в этой статье поделюсь своим способом решения головной боли, которая возникает при работе на проекте с несколькими таргетами.



Предвижу вопрос «зачем тебе, Саня, несколько таргетов в проекте?». Отвечаю: на проекте, где я когда-то работал, был один таргет, привязанный к аккаунту заказчика, так что:


  • доступа к этому аккаунту у нас не было;
  • отсутствовала возможность генерировать provision profiles и distribution сертификаты;
  • самое больное — не было возможности добавлять новые девайсы к provision profiles.

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


Решение было очевидным – создать второй таргет, который будет аналогичен главному, но иметь другой bundle id и который будет целиком и полностью привязан к нашему студийному аккаунту. Сказано – сделано! Второй таргет позволил вести полноценную разработку независимо от разработчиков на стороне заказчика, что не раз спасало нам процесс разработки, к примеру, когда на provision profile главного таргета разработчики заказчика удалили все наши девайсы и мы не могли запускать приложение на своих устройствах.


Несколько таргетов еще могут появиться в вашем проекте, когда нужно, например:


  • собрать несколько приложений из одной кодовой базы, но с разными ресурсами;
  • держать несколько инстансов приложения на одном устройстве.

Но работа на нескольких таргетах ведет к своим минусам, главный — необходимость строго контролировать процесс добавления новых файлов в проект. Они оба идентичны друг другу, соответственно, при добавлении нового файла в проект необходимо ставить галочку напротив обоих таргетов.



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


Если не придерживаться этого простого правила, после очередного мерджа вы можете обнаружить, что один из таргетов перестал собираться, а Xcode ругается, что не может найти тот или иной класс. Но самое интересное, если кто-то не добавил файл из Copy Bundle Resources в оба таргета (к примеру, xib ячейки), такую ошибку вы сможете обнаружить только лишь в runtime.


После очередной проблемы с Debug-таргетом, решили отдать контроль за консистентностью таргетов на скрипт. В качестве инструмента выбрали язык Ruby, так как для него есть отличный гем xcodeproj , который является чуть ли не стандартом для написания инструментов для iOS разработки (см. fastlane, generamba и т.п.).


В результате разработали скрипт, который реализует следующую логику работы:


  • на вход подаются путь до файла проекта и названия проверяемых таргетов;
  • по пути до проекта скрипт находит сам проект;
  • выбирает нужные таргеты, собирает файлы из них (Compile Sources, Copy Bundle Resources и Frameworks);
  • ищется разница между перечнями файлов;
  • выкидываем из этой разницы файлы из нашего whitelist. К примеру, файлы с константами, которые различаются в зависимости от таргета (baseUrl, если вы решили организовать проект так, чтобы приложения с разных таргетов обращались к разным серверам), либо файлы GoogleService-Info.plist, которые будут разными для разных таргетов.

Если скрипт находит разницу — у нас проблемы. Значит, тот или иной файл/фреймворк были добавлены в один таргет, но не добавлены в другой.


Таким образом, скрипт позволяет:


  • убедиться, что все файлы из Compile Sources и Copy Bundle Resources были корректно добавлены к обоим таргетам;
  • также проверяется идентичность списков добавленных фреймворков, что очень актуально, если на вашем проекте многомодульная архитектура;
  • интеграция скрипта на Build Phase позволит определить проблему еще до непосредственно стадии сборки проекта;
  • а если раньше вы для этого собирали на CI оба таргета, то теперь можно оставить сборку только одного, сократив время сборки на CI вдвое.

В случае ошибки, в Xcode вы сможете наблюдать следующее сообщение:



В случае же успеха — вас ждет единорожек:



Подробная инструкция добавления в проект, настройки, а также Example-project в репозитории.


Получившиеся утилита отлично показывает то, что даже будучи iOS-разработчиком можно внести небольшую дольку креатива в вашу повседневную работу и заняться автоматизацией и написанием скриптов, которые упростят вашу жизнь!


Спасибо за внимание!

Комментарии (14)


  1. ki11er
    18.12.2019 20:43

    Проблемы, которые вы описали, решаются через связку .xcconfig + configurations + schemes. При этом получаем еще и возможность легко конфигурировать сборки под разное окружение, прокинув параметры из .xcconfig через Info.plist в приложение.
    Таргеты, хоть и решают эти задачи, предназначены для другого и действительно, добавляют много головной боли в таком сценарии использования.


    1. Chausov_surf Автор
      18.12.2019 20:52

      В свое время в студии мы проводили исследование, где выясняли, что нам лучше подходит для разделения сборок под разное окружение, в котором в том числе рассматривался подход и с .xcconfig, и со схемами, и с разными таргетами. Выбор тогда пал на использование таргетов, так как описанный подход позволяет более явно разделять файлы (вроде GoogleService-Info.plist) между таргетами, плюс на тот момент в разработке были проекты, где нужны были реально разные инстансы приложений (случай брендирования одного приложения с дополнением различной логики в зависимости от конкретного таргета). Но соглашусь с вами, это добавляет головной боли, и в конкретно данном случае вполне мог подойти подход с использованием указанной вами связки)


      1. ki11er
        19.12.2019 05:20

        Вот тут уже соглашусь, для этих задач таргеты вполне подходят.
        Хотя, я бы задумался о необходимости вести единый проект в рамках ребрендинга, когда речь уже идет о серьезных различиях в реализациях. Тут, скорее, нужно смотреть в сторону разделения репозиториев, модульной архитектуры и feature toggles. Но это, конечно, если планировать все заранее — большой legacy-проект, как вы написали ниже, на эти рельсы быстро не поставить.


  1. izack
    18.12.2019 20:55

    Прошу прощения, а что мешает сделать библиотеку и совместный код в нее добавлять?


    1. Chausov_surf Автор
      18.12.2019 21:03

      А не могли бы вы более детально раскрыть суть вопроса? Вся кодовая база у нас естественно лежит в общем репозитории, с которым работает вся команда. Если же вы имели в виду, что можно весь проект разбить на отдельные модули, в рамках которых описанных проблем бы не возникло, — то данный подход не рассматривался, ибо второй таргет был добавлен «на коленке» в свободное от работы время, а времени на внедрение многомодульной архитектуры не было) Да и нецелесообразно это было, в силу большого количества легаси-кода на проекте, и как следствие большой сложности и огромного количества импакта


      1. izack
        18.12.2019 21:42

        Я имел ввиду что создать еще библиотеку, которая будет включена в оба проекта. При этом весь общий код включать в нее, а внешние связи объявляются в виде интерфейсов(пример — UITableView, UICollection). На подобии Cocoapods, что по сути является библиотекой. И при этом делать сторонний проект не обязательно.


        1. Chausov_surf Автор
          19.12.2019 13:27

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


  1. Awaking
    19.12.2019 06:15

    Присоединяюсь. Таргеты таки для других целей. Вы хотите менять бандл (по сути часть конфигурации проекта), и настройка должна касаться только проекта, а создание таргета приводит к параметризации вообще всех файлов. Отсюда и ваша головная боль, и человеческий фактор.
    Мой подход — настройка окружения (endpoints, bundle, api keys и тп) — через .xcconfig, каждый из которых соответствует определенной билд конфигурации.
    Для брендирования — уже отдельные таргеты. Все это отлично совмещается с fastlane.


    1. Chausov_surf Автор
      19.12.2019 13:34

      Чувствую, пора еще раз посмотреть в сторону .xcconfig)
      Только один вопрос есть. Где-то полгода назад на онбординге один разработчик как-то пытался реализовать такой подход, но завяз с тем, что разные бандлы => разные проекты в firebase => разные .plist файлы для конфигурации аналитики. И реализовать корректное их подтягивание в зависимости от конфигурации у него с ходу не получилось. Может быть у вас есть какие-то рецепты для таких случаев?


      1. OlKir
        19.12.2019 13:55

        При инициализации Firebase можно ей прямо указать какой plist использовать. Единственно в документации предупреждают, что это нужно делать как можно раньше, чтоб не потерять события которые собираются на старте приложения.

        let options = FirebaseOptions.init(contentsOfFile: optionFilePath) {
        FirebaseApp.configure(options: options)
        }


        1. Chausov_surf Автор
          19.12.2019 14:04

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


      1. ki11er
        20.12.2019 09:27

        Можно подложить нужный .plist скриптом в Build Phases


        1. ki11er
          20.12.2019 09:30

          Хотя, вариант OlKir мне нравится больше и отлично ложится на концепцию .xcconfig. Спасибо, не знал о такой возможности инициализации Firebase.


  1. OlKir
    19.12.2019 13:30

    Спасибо, что поделились! Отдельно порадовали эмоджи в консоли.