В МойОфис мы создаем продукты для совместной работы и делового общения. В том числе стремимся делать так, чтобы доступ к корпоративной коммуникации был максимально удобным для пользователя. Большинство наших решений — от редакторов документов и почтовых систем до цифрового рабочего пространства 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
  • Git:

# из мастер-ветки
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
Фазы сборки в 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
Структура папок в проекте 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.

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


  1. Mox
    31.05.2023 14:07

    Он же прям из коробки в RN используется?


    1. SalaarFiend Автор
      31.05.2023 14:07
      +1

      Да, RN использует по умолчанию cocoapods для iOS зависимостей