В жизни многих компаний, которые имеют и развивают свой стек библиотек и компонентов, наступает момент, когда объёмы этого стека становится сложно поддерживать.
В случае разработки под платформу iOS, да и в целом, экосистему Apple, есть два варианта подключать библиотеки в качестве зависимостей:
- Собирать их каждый раз при сборке приложения.
- Собирать их заранее, используя уже собранные зависимости.
При выборе второго подхода становится логичным использовать CI/CD системы для сборки библиотек в готовые к употреблению артефакты.
Однако, необходимость собирать библиотеки под несколько платформ или архитектур процессора в экосистеме Apple, зачастую, требует проводить не всегда тривиальные операции, как при сборке библиотеки, так и конечного продукта, который её использует.
На этом фоне, было сложно не заметить и крайне интересно изучить, одно из нововведений от Apple, представленное на WWDC 2019 в рамках презентации Binary Frameworks in Swift — формат упаковки фреймворков — XCFramework.
XCFramework имеет несколько преимуществ, в сравнении с устоявшимися подходами:
- Упаковка зависимостей под все целевые платформы и архитектуры в единый bundle из коробки.
- Подключение bundle в формате XCFramework, как единой зависимости для всех целевых платформ и архитектур.
- Отсутствие необходимости в сборке fat/universal фреймворка.
- Нет необходимости избавляться от x86_64 слайсов (slice) перед загрузкой конечных приложений в AppStore.
В этой статье мы расскажем, зачем был внедрён этот новый формат, что он из себя представляет, а также, что он даёт разработчику.
Как появился новый формат
Ранее компанией Apple был выпущен менеджер зависимостей Swift Package Manager.
Суть в том, что Swift PM позволяет выполнять поставку библиотек в форме открытого исходного кода с описанием зависимостей.
С позиции разработчика, поставляющего библиотеку, хотелось бы выделить два аспекта Swift PM.
- Очевидный минус — по тем или иным причинам, не все поставщики библиотек хотели бы открывать их исходный код потребителям.
- Очевидный плюс — при компиляции зависимостей из исходников мы избавляемся от необходимости соблюдать бинарную совместимость библиотек.
XCFramework Apple предлагает, как новый бинарный формат упаковки библиотек, рассматривая его как альтернативу Swift Packages.
Этот формат, как и возможность подключения собранной в XCFramework библиотеки, доступен, начиная с Xcode 11 и его beta-версий.
Что из себя представляет XCFramework
По своей сути, XCFramework — новый способ упаковки и поставки библиотек, в их различных вариантах.
Кроме прочего, новый формат также позволяет производить и упаковку статических библиотек вместе с их заголовочными файлами, в том числе и написанных на С/Objective-C.
Рассмотрим формат несколько подробнее.
Упаковка зависимостей под все целевые платформы и архитектуры в единый bundle из коробки
Все сборки библиотеки под каждую из целевых платформ и архитектур теперь могут быть упакованы в единый bundle с расширением .xcframework.
Однако, для этого, на данный момент времени, приходится использовать скрипты для вызова командыxcodebuild
с новым для Xcode 11 ключом-create-xcframework
.
Процесс сборки и упаковки рассмотрим далее.
Подключение bundle в формате XCFramework, как единой зависимости для всех целевых платформ и архитектур
Поскольку bundle .xcframework содержит все необходимые варианты сборки зависимости, нам не нужно заботиться о его архитектуре и целевой платформе.
В Xcode 11 библиотека, упакованная в .xcframework подключается точно также, как и обычный .framework.
Говоря конкретнее, этого можно добиться следующими способами в настройках target’а:
- добавляя .xcframework в раздел «Frameworks And Libraries» на вкладке «General»
- добавляя .xcframework в «Link Binary With Libraries» на вкладке «Build Phases»
Отсутствие необходимости в сборке fat/universal фреймворка
Ранее для поддержки в подключаемой библиотеке нескольких платформ и нескольких архитектур приходилось подготавливать так называемые fat или universal фреймворки.
Производилось это с помощью командыlipo
для сшития всех вариантов собранного фреймворка в единый толстый бинарник.
Подробнее с этим можно ознакомиться, к примеру, в следующих статьях:
- Как собрать собственный фреймворк для iOS (habr.ru)
- Writing Custom Universal Framework in Xcode 10.2 and iOS 12 (medium.com)
Нет необходимости избавляться от x86_64 слайсов (slice) перед загрузкой конечных приложений в AppStore
Обычно такой слайс используется для обеспечения работы библиотек в симуляторе iOS.
При попытке загрузить приложение с зависимостями, содержащими x86_64 слайс в AppStore можно столкнуться с небезызвестной ошибкой ITMS-90087.
Создание и упаковка XCFramework: теория
В упомянутой ранее презентации, описаны несколько шагов, которые требуются для сборки и упаковки библиотеки в формате XCFramework:
Подготовка проекта
Для начала во всех target’ах проекта, которые отвечают за сборку библиотеки под целевые платформы, нужно включить новую для Xcode 11 настройку — «Build Libraries for Distribution».
Сборка проекта под целевые платформы и архитектуры
Далее нам предстоит собрать все таргеты под целевые платформы и архитектуры.
Рассмотрим примеры вызова команд на примере определённой конфигурации проекта.
Допустим, что в проекте у нас есть две схемы «XCFrameworkExample-iOS» и «XCFramework-macOS».
Также в проекте есть два target’а, которые собирают библиотеку под iOS и под macOS.
Для сборки всех требуемых конфигураций библиотеки нам потребуется собрать оба target’а, используя соответствующие им схемы.
Однако, для iOS нам потребуется две сборки: одна под конечные устройства (ARM), а другая под симулятор (x86_64).
Итого нам потребуется собрать 3 фреймворка.
Для этого можно воспользоваться командой
xcodebuild
:
# iOS devices xcodebuild archive \ -scheme XCFrameworkExample-iOS \ -archivePath "./build/ios.xcarchive" \ -sdk iphoneos \ SKIP_INSTALL=NO # iOS simulator xcodebuild archive \ -scheme XCFrameworkExample-iOS \ -archivePath "./build/ios_sim.xcarchive" \ -sdk iphonesimulator \ SKIP_INSTALL=NO # macOS xcodebuild archive \ -scheme XCFrameworkExample-macOS \ -archivePath "./build/macos.xcarchive" \ SKIP_INSTALL=NO
В итоге у нас получилось 3 собранных фреймворка, которые далее мы будем упаковывать в контейнер .xcframework.
Упаковка собранных .framework в .xcframework
Сделать это можно следующей командой:
xcodebuild -create-xcframework \ -framework "./build/ios.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \ -framework "./build/ios_sim.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \ -framework "./build/macos.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \ -output "./build/XCFrameworkExample.xcframework"
Здесь может быть множество указаний параметра
-framework
, которые указывают на все сборки .framework, которые требуется вложить в контейнер .xcframework.
Подготовка проекта библиотеки для будущей сборки и упаковки XCFramework
TL;DR: готовый проект можно скачать в репозитории на Github.
В качестве примера реализуем библиотеку, которая будет доступна под две платформы: iOS и macOS.
Воспользуемся упомянутой в предыдущем разделе статьи конфигурацией проекта: две схемы и два соответствующих им Framework target’а для платформ iOS и macOS.
Сама же библиотека будет предоставлять нам простенький extension для String?
(Optional where Wrapped == String
), с единственным свойством.
Назовём это свойство isNilOrEmpty
и, как следует из названия, оно позволит нам узнать, когда внутри String?
отсутствует значение или хранимая внутри строка является пустой.
Код можно реализовать следующим образом:
public extension Optional where Wrapped == String {
var isNilOrEmpty: Bool {
if case let .some(string) = self {
return string.isEmpty
}
return true
}
}
Приступим непосредственно к созданию и конфигурации проекта.
Для начала нам нужно создать проект типа «Framework» под одну из двух целевых платформ на ваш выбор: iOS или macOS.
Сделать это в Xcode можно через пункт меню «File» => «New» => «Project», либо сочетанием клавиш ⇧ + ⌘ + N (по умолчанию).
Далее наверху диалога выбираем нужную платформу (iOS или macOS), выбираем тип проекта Framework и переходим далее по кнопке «Next».
На следующем экране нам нужно задать имя проекта в поле «Product Name».
Как вариант можно использовать «базовое» имя проекта, в упомянутой ранее конфигурации это «XCFrameworkExample».
В дальнейшем, при конфигурации проекта к базовому имени, использованному в имени target’ов мы добавим суффиксы, обозначающие платформы.
После этого требуется создать ещё один Target типа «Framework» в проекте под другую из перечисленных платформ (кроме той, под которую проект был создан изначально).
Для этого можно воспользоваться пунктом меню «File» => «New» => «Target».
Далее выбираем в диалоге другую (относительно выбранной в пункте 1) из двух платформ, после чего снова выбираем тип проекта «Framework».
Для поля «Product Name» мы можем сразу использовать название с суффиксом платформы, для которой мы в данном пункте добавляем target. Так, если платформа — это macOS, то имя может быть «XCFrameworkExample-macOS» (%base_name%-%platform%).
Настроим таргеты и схемы, чтобы их было проще отличать.
Для начала переименуем наши схемы и привязанные к ним target’ы таким образом, чтобы их имена отображали платформы, например так:
- «XCFrameworkExample-iOS»
- «XCFrameworkExample-macOS»
Далее добавляем в проект .swift файл с кодом нашего extension’а для
String?
Добавим в проект новый .swift файл с именем «Optional.swift».
И в сам файл помещаем ранее упомянутый extension дляOptional
.
Важно не забыть добавить файл с кодом к обеим target’ам.
Теперь у нас есть проект, который мы можем собрать в XCFramework используя команды из предыдущего этапа.
Процесс сборки и упаковки библиотеки в формат .xcframework
Для сборки библиотеки и упаковки её в формат .xcframework на данном этапе можно воспользоваться bash-скриптом в отдельном файле. К тому же, это позволит в будущем использовать эти наработки для интеграции решения в CI/CD системы.
Скрипт выглядит до безобразия просто и, по факту, сводит воедино ранее упомянутые команды для сборки:
#!/bin/sh
# ----------------------------------
# BUILD PLATFORM SPECIFIC FRAMEWORKS
# ----------------------------------
# iOS devices
xcodebuild archive \
-scheme XCFrameworkExample-iOS \
-archivePath "./build/ios.xcarchive" \
-sdk iphoneos \
SKIP_INSTALL=NO
# iOS simulator
xcodebuild archive \
-scheme XCFrameworkExample-iOS \
-archivePath "./build/ios_sim.xcarchive" \
-sdk iphonesimulator \
SKIP_INSTALL=NO
# macOS
xcodebuild archive \
-scheme XCFrameworkExample-macOS \
-archivePath "./build/macos.xcarchive" \
SKIP_INSTALL=NO
# -------------------
# PACKAGE XCFRAMEWORK
# -------------------
xcodebuild -create-xcframework \
-framework "./build/ios.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
-framework "./build/ios_sim.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
-framework "./build/macos.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
-output "./build/XCFrameworkExample.xcframework"
Содержимое .xcframework
В результате работы скрипта cборки из предыдущего пункта статьи, мы получаем заветный bundle .xcframework, который можно добавлять в проект.
Если мы заглянем внутрь этого bundle, который, как и .framework, является по сути простой папкой, то увидим следующую структуру:
Здесь мы видим, что внутри .xcframework лежат сборки в формате .framework, разбитые по платформам и архитектурам. Также для описания содержимого bundle .xcframework, внутри имеется файл Info.plist.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>XCFrameworkExample.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-x86_64-simulator</string>
<key>LibraryPath</key>
<string>XCFrameworkExample.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>macos-x86_64</string>
<key>LibraryPath</key>
<string>XCFrameworkExample.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>
Можно заметить, что для ключа «CFBundlePackageType», в отличии от формата .framework используется новое значение «XFWK», а не «FMWK».
Итоги
Итак, формат упаковки библиотек в XCFramework есть ни что иное, как обычный контейнер для библиотек собираемых в формате .framework.
Однако, такой формат позволяет отдельно хранить и независимо использовать каждую из архитектур и платформ, представленных внутри. Это избавляет от ряда проблем, присущих широко распространённому подходу со сборкой fat/universal фреймворков.
Как бы то ни было, на данный момент, есть немаловажный нюанс, касающийся вопроса использования XCFramework в реальных проектах — управление зависимостями, которое в формате XCFramework компанией Apple не реализовано.
Для этих целей привычно используются Swift PM, Carthage, CocoaPods и прочие системы управления зависимостями и их сборки. Поэтому неудивительно, что поддержка нового формата уже находится на стадии реализации именно в проектах CocoaPods и Carthage.