Упрощаем разработку на React Native: чем полезен CocoaPods?
В МойОфис мы создаем продукты для совместной работы и делового общения. В том числе стремимся делать так, чтобы доступ к корпоративной коммуникации был максимально удобным для пользователя. Большинство наших решений — от редакторов документов и почтовых систем до цифрового рабочего пространства Squadus — представлены, помимо десктопа и веба, на основных мобильных платформах.
iOS- и Android-приложения Squadus мы разрабатываем с помощью кроссплатформенного фреймворка React Native. И сегодня расскажем о том, какое значение в iOS-разработке имеет CocoaPods — мощный инструмент управления нативными iOS-зависимостями, который позволяет упростить управление вашим проектом.
Под катом разбираем основы работы с CocoaPods, а также пример его использования в проекте для исправления ошибки.
Привет, Хабр! Меня зовут Вячеслав Чащухин, в МойОфис я разработчик и занимаюсь мобильной версией Squadus — нового решения для деловых коммуникаций. Ниже я рассказываю о том, что и как делает CocoaPods (или же просто Pods) в наших React Native проектах. Статья будет интересна тем, кто только начинает погружаться в эту тему или просто хочет узнать немного больше.
Что такое CocoaPods?
Это менеджер зависимостей для приложений iOS. Он собирает конфигурации и исходный код зависимостей и связывает их с рабочим пространством в Xcode. Также отвечает за разрешение конфликтов между библиотеками.
Вся эта конфигурация использует файл Podfile, в котором мы указываем, для каких тегов проекта будут установлены те или иные зависимости. Конфигурация React Native для iOS также происходит в этом файле, её можно найти по имени use_react-native
.
Podfile
Коротко скажу об основах конфигурации. Стандартный сгенерированный Podfile обычно содержит несколько обязательных элементов.
platform :ios, '14.0' # <- Для какой версии iOS ведется разработка
project 'ExampleApp' # <- Имя проекта
target 'ExampleApp' do # <- Таргет в iOS-проекте
pod 'AwesomeLib', '1.1.13' # <- Библиотека, которую мы хотим подключить к приложению
end
post_install do |installer| # <- Процесс после установки библиотек
someThingPostIstall(installer)
end
Остановлюсь подробнее на targets и установке для них отдельных pods.
Targets
Target в проекте — это сущность, которая точно определяет, какой продукт будет собран, содержит инструкции для сборки проекта из набора файлов воркспейса или проекта.
Если вы хотите установить зависимости для определенного target, то должны ввести их в соответствующий target в Podfile.
project 'ExampleApp'
pod 'LibForAllTargets', '1.0.0'
target 'ExampleApp' do
pod 'AwesomeLib', '1.1.13'
end
target 'ExampleAppWidget' do
pod 'AwesomeLibForWidget'
end
Бывают кейсы, когда вам нужно создать другой target, который должен иметь тот же набор зависимостей. Например, target для тестов.
project 'ExampleApp'
target 'ExampleApp' do
pod 'AwesomeLib', '1.1.13'
target 'ExampleAppTests' do
inherit! :complete # <- Указывает на наследование всего поведения от родителя
# Pods for testing
end
end
target 'ExampleAppWidget' do
pod 'AwesomeLibForWidget'
end
Установка pods
Обычно мы устанавливаем 3rd-party библиотеки с помощью yarn или npm, а React Native сам выполняет работу по их подключению к проекту (спасибо механизму автоматической линковки, начиная с версии 0.60).
Но бывают случаи, когда нам необходимо подключить их самостоятельно. Для этого нужно знать, каким образом мы можем конфигурировать устанавливаемый pod.
Мы можем установить pod определенной версии, определенной ветки, тега или даже коммита:
Версии:
pod 'AwesomePod', '1.1.1' # <- Конкретная версия
pod 'ExamplePod', '~> 0.4.3' # <- Версия 0.4.3 и версии до 0.5, не включая 0.5
# из мастер-ветки
pod 'AwesomePod', :git => 'https://github.com/AwesomePod/AwesomePod.git'
# из конкретного коммита
pod 'AwesomePod', :git => 'https://github.com/AwesomePod/AwesomePod.git', :commit => '0b102a5c41'
Путь к файлу:
# из файла
pod 'AwesomePod', :path => '~/Documents/AwesomePod'
Для получения более подробной информации по установке, рекомендую прочитать руководство по CocoaPods.
post_install
Этот раздел в Podfile может выглядеть немного сложнее, чем описанные выше. Он нужен для очистки мусора, настройки окружения или чего-либо еще, что необходимо сразу после установки pods.
Например, в Podfile, созданном с помощью react-native init, вы можете найти следующие строки:
post_install do |installer|
react_native_post_install(installer)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
end
В данном случае react_native-post_install
— устанавливает exclude architectures, исправляет пути поиска библиотек, устанавливает необходимые флаги.
Также в post_install
мы можем установить необходимые для проекта флаги, чтобы установка следовала определенной последовательности действий.
Например, вы можете увидеть нечто подобное в некоторых проектах:
post_install do |installer|
react_native_post_install(installer)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
end
end
end
Здесь скрипт проходит по каждой target проекта и устанавливает флаг настроек сборки APPLICATION_EXTENSION_API_ONLY
в false.
Если вам нужно добавить в Podfile установку определенной зависимости, важно быть внимательным к тому, что вы устанавливаете.
Чтобы избежать проблем, стоит знать несколько вещей:
React Native Podfile содержит также конфигурацию зависимостей, необходимых для работы фреймворка. Например, Yoga, glog, boost. Конечно, трудно представить ситуацию, когда эти зависимости придется устанавливать отдельно. Но помнить об этом стоит.
Полный список зависимостей можете посмотреть здесь: node_modules/react-native/scripts/react_native_pods.rb
→use_react_native
Вы можете столкнуться с конфликтом версий между установленным pod и тем, который указан как зависимость в одном из установленных пакетов npm. Это довольно легко решить: исправьте файл с расширением
.podspec
в пакете или измените установленную версию.Если вы хотите установить группу зависимостей, которые каким-то образом связаны друг с другом, вы должны знать, что pod устанавливаются в порядке сверху вниз.
Для удобства я рекомендую группировать pods по назначению. Пример:
project 'ExampleApp'
def firebase_pods
pod 'Firebase'
pod 'FirebaseCore'
pod 'FirebaseCoreInternal'
end
def npm_packages_depend_pods
pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
end
target 'ExampleApp' do
firebase_pods
npm_packages_depend_pods
pod 'AwesomeLib', '1.1.13'
end
target 'ExampleAppWidget' do
pod 'AwesomeLibForWidget'
end
Углубляемся в тему
Немного теории о том, как CocoaPods обрабатывает зависимости в наших проектах React Native.
После выполнения команды pod install
, CocoaPods просматривает все pods, скачивает и устанавливает необходимые версии. Затем он генерирует определенное количество вспомогательных скриптов (таких как frameworks.sh & resourses.sh), которые затем встраиваются в фазы сборки, — их вы можете увидеть в XCode.
В общих чертах фазы сборки приложения для iOS выглядят так:
Фазы сборки в XCode
Вместо рассказа о всех фазах сборки, остановлюсь на двух конкретных, — чтобы показать, как работают pods.
[CP] Embed Pods Framework
Это фаза, в которой выполняется скрипт и которую можно найти по пути, где ExampleApp — имя вашего таргета.
"${PODS_ROOT}/Target Support Files/Pods-ExampleApp/Pods-ExampleApp-resources.sh"
Чтобы найти эту папку: Откройте Xcode → Перейдите в проект Pods (он появится после установки pods при вызове команды pod install
) → Targets Support Files → Pods-${YourTargetName}
Структура папок в проекте Pods
Открыв этот файл, вы увидите скрипт, который создает необходимые папки, устанавливает пути. Но самое важное для нас место — как раз то, где происходит установка фреймворков в приложения, основанная на конфигурации сборки (Debug, Release и т. д.).
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/Giphy/GiphyUISDK.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework"
fi
Здесь мы видим, что зависимости Flipper (если они у вас установлены) входят только в конфигурацию Debug.
[CP] Copy Pods Resources
Фаза, в которой выполняется скрипт и которую можно найти по пути, где ExampleApp — имя вашего таргета.
"${PODS_ROOT}/Target Support Files/Pods-ExampleApp/Pods-ExampleApp-resources.sh"
Чтобы найти эту папку: Откройте Xcode → Перейдите в проект Pods (он появится после установки pods при вызове команды pod install
) → Targets Support Files → Pods-${YourTargetName}
Этот скрипт устанавливает необходимые папки при запуске и переносит ресурсы из наших пакетов npm в нужные пути, именно поэтому вы всегда должны запускать pod install
после добавления пакетов. Он также разделяет все на конфигурации (Debug, Release и т.д.).
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle"
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf"
install_resource "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf"
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle"
install_resource "${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle"
fi
Здесь вы можете увидеть, например, какие шрифты импортирует библиотека. Если она использует какие-то пакеты для своей работы, они также будут здесь.
Переходим к практике
После того как мы ознакомились с принципами работы pods, опишу один практический пример. Ниже — рассказ о проблеме, с которой я справился бы в 4 раза быстрее, если б на момент её решения знал всё то, что рассказал ранее в статье.
Dyld library not loaded, error on iOS (React-Native)
С этой ошибкой я столкнулся после включения в проект Flipper.
DYLD 1 Library missing
Library not loaded: @rpath/OpenSSL.framework/OpenSSL
Мне потребовалось много времени, чтобы найти подходящее решение. В проекте была еще библиотека react-native-simple-crypto, которая также использовала pod OpenSSL-Universal в качестве зависимости для своих нужд. При этом Flipper непосредственно использует эту зависимость.
Я искал решение в различных источниках, в том числе на Github. В результате я нашел ответ на вопрос, где и как проверить поведение фреймворка при сборке приложения.
Нам понадобятся как раз те самые вспомогательные файлы, которые мы находили в описанных выше фазах сборки.
Как мы уже знаем, файл Pods-ExampleApp-resourses.sh
содержит автоматически сгенерированный скрипт, который используется для установки фреймворков в проект в соответствующей фазе сборки.
Здесь я заметил проблему в том, что нужный мне фреймворк OpenSSL-Universal устанавливается исключительно в конфигурацию Debug.
Мой файл выглядел примерно так:
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework"
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework"
fi
Убедившись, что фреймворк действительно отсутствует, я перешел к исправлению.
Во-первых, проверил свой Podfile. Установка библиотек и фреймворков происходит сверху вниз. То, что было установлено последним, перезаписывает конфигурацию предыдущего. Я не могу показать вам фактический Podfile проекта, поскольку он содержит конфиденциальную информацию, но давайте рассмотрим некоторые примеры.
Полный скрипт use_flipper
доступен по пути:
node_modules/react-native/scripts/react_native_pods.rb
Podfile до изменений:
# node_modules/react-native/scripts/react_native_pods.rb
def use_flipper!()
...other pods install...
# устанавливает только для Debug конфигурации
pod 'OpenSSL-Universal', :configurations => ['Debug']
end
# ios/Podfile
def all_pods
pod 'OpenSSL-Universal',:configurations => ['Debug','Release']
use_flipper!()
end
В этой ситуации блок use_flipper!() перезапишет ранее установленный pod и установит конфигурацию только для Debug-сборок.
Исходя из этого я понял, что для устранения проблемы достаточно после установки use_flipper поставить установку pod OpenSSL-Universal.
После изменений:
# node_modules/react-native/scripts/react_native_pods.rb
def use_flipper!()
...other pods install...
# устанавливает только для Debug конфигурации
pod 'OpenSSL-Universal', :configurations => ['Debug']
end
# ios/Podfile
def all_pods
use_flipper!()
pod 'OpenSSL-Universal',:configurations => ['Debug','Release','Staging']
end
Таким образом, последняя установка pod перезаписала установку use_flipper, и я получил работающее приложение — без нарушения работы Flipper на отладочных сборках и без потери OpenSSL для react-native-simple-crypto.
***
Будем рады увидеть в комментариях ваши мысли по поводу CocoaPods и личного опыта работы с этим менеджером зависимостей. Также, пожалуйста, сообщите, если вам интересно узнать больше о нашей React Native разработке: постараемся учесть это при подготовке новых хабр-статей.
Если же вы опытный мобильный разработчик, советуем обратить внимание на открытые в МойОфис вакансии. Сейчас мы ищем талантливых специалистов, которые помогут нам развивать решения как на iOS, так и на Android.