Irbisa · Шпаргалка

Сборка Xcode и модуляризация

Таргеты, схемы, xcconfig, SPM, статическая и динамическая линковка, XCFramework, время сборки

Senior-разработчик · 22 вопроса

  1. 01

    Чем отличаются target, scheme и build configuration, и чем .xcodeproj отличается от .xcworkspace?

    Target описывает один продукт сборки (приложение, фреймворк, тестовый бандл), build configuration представляет собой именованный набор build settings вроде Debug или Release, а scheme говорит, какие таргеты собирает каждое действие (Run, Test, Profile, Archive) и с какой конфигурацией.

  2. 02

    Как выбрать минимальную версию iOS (deployment target) и как пользоваться новыми API, не поднимая её?

    Берите самую старую версию iOS, на которой ещё сидит заметная доля ваших пользователей (обычно текущий мажорный релиз минус один-два), а новые API вызывайте за проверками if #available и аннотациями @available.

    import SwiftUI struct RootView: View {  var body: some View {    if #available(iOS 18, *) {      // runtime check: only this branch may use the iOS 18 Tab API…
  3. 03

    Что такое сертификат подписи, provisioning profile и entitlements, и как они связаны между собой?

    Сертификат доказывает, кто подписал приложение, entitlements перечисляют возможности, которые приложение заявляет (push, App Groups, общий Keychain), а provisioning profile представляет собой подписанное Apple разрешение, связывающее App ID, сертификаты, entitlements и (для разработки и Ad Hoc) конкретные устройства.

  4. 04

    Чем отличаются CocoaPods, Carthage и Swift Package Manager, и почему большинство команд перешли на SPM?

    CocoaPods генерирует workspace и собирает поды из центрального репозитория спек, Carthage только собирает фреймворки, которые вы подключаете сами, а SPM является собственным менеджером зависимостей Apple, встроенным в Swift и Xcode: без отдельного инструмента и без сгенерированного workspace.

  5. 05

    Что делает эта функция в Debug-сборке и в Release-сборке, и почему поведение различается?

    func loadItem(id: Int?) -> String {  assert(id != nil, "id is required")  #if DEBUG  print("loading \(String(describing: id))")  #endif  precondition((id ?? 0) >= 0, "negative id")  return "item-\(id ?? -1)"} print(loadItem(id: nil))

    В Debug assert не проходит, и приложение падает с «Assertion failed: id is required»; в Release assert вырезан при компиляции, а блока #if DEBUG не существует, поэтому вызов молча печатает item--1.

  6. 06

    Зачем нужны .xcconfig-файлы и в каком порядке Xcode вычисляет build setting?

    .xcconfig представляет собой текстовый файл с build settings, привязанный к конфигурации: настройки живут в строках, которые можно ревьюить и мержить, а не внутри project.pbxproj, который Xcode переписывает как машинный блоб и на котором две ветки обязательно сконфликтуют.

    // Config/Base.xcconfig: shared by every configurationSWIFT_VERSION = 6.0IPHONEOS_DEPLOYMENT_TARGET = 17.0CODE_SIGN_STYLE = Automatic // Config/Staging.xcconfig: assigned to the Staging configuration…
  7. 07

    Что происходит, когда вы нажимаете Build в Xcode: от графа задач до подписанного приложения?

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

  8. 08

    Чем в Package.swift отличаются target, product и package dependency, и почему другой пакет не может импортировать ваш таргет?

    Target задаёт один модуль, product определяет то, что пакет экспортирует наружу, package dependency указывает на другой пакет, который вы используете; ваш таргет снаружи не виден, пока его не включил library-продукт, сколько бы в нём ни было public.

    // swift-tools-version: 6.0import PackageDescription let package = Package(  name: "Networking",  platforms: [.iOS(.v17)],…
  9. 09

    Как Swift Package Manager выбирает версии зависимостей и почему Package.resolved нужно коммитить?

    SwiftPM собирает требования к версиям от всех пакетов графа и выбирает одну версию на пакет (самую новую, которая удовлетворяет всем), а затем записывает точные версии и ревизии в Package.resolved.

    // swift-tools-version: 6.0import PackageDescription let package = Package(  name: "AppModules",  dependencies: […
  10. 10

    Что такое XCFramework и почему он заменил fat-фреймворки, собранные через lipo?

    .xcframework представляет собой бандл, в котором рядом лежат сборки библиотеки под каждую платформу и вариант (устройство, симулятор, Mac Catalyst) плюс Info.plist, по которому Xcode выбирает нужную.

    set -euo pipefail # One archive per platform variant: device and simulator are both arm64,# so lipo cannot merge them into one binary.xcodebuild archive -scheme Analytics \  -destination "generic/platform=iOS" -archivePath build/ios \…
  11. 11

    SwiftLint и кодогенератор запускаются на каждой сборке, даже когда ничего не менялось: чего не хватает системе сборки и как это исправить?

    Run Script-фазу без объявленных входных и выходных файлов невозможно признать актуальной, поэтому система сборки запускает её каждый раз, а если она пишет файл, который читает компилятор, перекомпилируется и всё, что ниже по графу.

    # Run Script phase, wrong: no inputs or outputs, so it runs on every build,# and rewriting Strings.swift recompiles everything that uses it.#   swiftlint --fix#   Tools/gen-strings Resources/Localizable.xcstrings > Sources/Generated/Strings.swift # Run Script phase, right.…
  12. 12

    Что такое app thinning и почему размер универсального .ipa, размер загрузки для устройства и размер после установки различаются?

    App thinning означает, что App Store собирает из вашей загрузки вариант под каждое устройство: slicing оставляет одну архитектуру, один масштаб ассетов и подходящие GPU-ресурсы, поэтому пользователь скачивает сжатый вариант, намного меньший универсального .ipa, а устанавливает более крупный несжатый.

    set -euo pipefail xcodebuild archive -workspace App.xcworkspace -scheme App \  -configuration Release -destination 'generic/platform=iOS' \  -archivePath build/App.xcarchive…
  13. 13

    Что такое privacy manifest (PrivacyInfo.xcprivacy) и что будет со сборкой без него?

    PrivacyInfo.xcprivacy представляет собой plist в составе приложения или SDK, который объявляет, какие данные собираются, есть ли трекинг, через какие домены он идёт и зачем вызываются API из списка «required reason».

    <?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>NSPrivacyTracking</key>  <false/>…
  14. 14

    Как сборка попадает из Xcode в App Store и что по пути происходит в TestFlight и на App Review?

    Вы архивируете Release-сборку, подписываете её для дистрибуции и загружаете в App Store Connect, где она обрабатывается и может уйти тестировщикам TestFlight; затем вы привязываете её к версии приложения, отправляете на App Review и выпускаете вручную, автоматически или поэтапно.

  15. 15

    Как настроить CI/CD для iOS-приложения и как решить подпись кода на CI-машине?

    CI запускает тот же xcodebuild (обычно через fastlane или Xcode Cloud) на macOS-раннере, и самое сложное здесь подпись: раннеру нужны приватный ключ distribution-сертификата и подходящий профиль, причём без участия чьего-то Mac или личного Apple ID.

    # fastlane/Fastfiledefault_platform(:ios) platform :ios do  lane :beta do    setup_ci # temporary keychain on CI runners, no-op locally…
  16. 16

    Чем статическая линковка отличается от динамической на iOS и почему холодный запуск замедлился, когда двенадцать модулей стали динамическими фреймворками?

    Статическая библиотека копируется в бинарник при линковке и ничего не стоит при запуске, а каждый встроенный динамический фреймворк является отдельным Mach-O-образом, который dyld должен найти, отобразить в память, пропатчить и инициализировать до main: на двенадцать фреймворков эта работа выполняется двенадцать раз.

  17. 17

    В этом манифесте динамические фреймворки Feed и Profile оба зависят от Analytics: что пойдёт не так в рантайме и как это исправить?

    let package = Package(  name: "App",  products: [    .library(name: "Analytics", type: .static, targets: ["Analytics"]),    .library(name: "Feed", type: .dynamic, targets: ["Feed"]),    .library(name: "Profile", type: .dynamic, targets: ["Profile"]),  ],  targets: [    .target(name: "Analytics"),  // holds AnalyticsTracker.shared    .target(name: "Feed", dependencies: ["Analytics"]),    .target(name: "Profile", dependencies: ["Analytics"]),  ])

    Analytics объявлен как type: .static, поэтому его код, включая AnalyticsTracker.shared, копируется и в Feed, и в Profile.

  18. 18

    Изменение одной строки в фича-модуле стоит двенадцати минут сборки: как выяснить, что на самом деле пересобирается, и исправить это?

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

    set -euo pipefailSIM='generic/platform=iOS Simulator' # 1. Where does the time go? Per-task durations (open the Build Timeline in Xcode too)xcodebuild -workspace App.xcworkspace -scheme App -destination "$SIM" \  build -showBuildTimingSummary…
  19. 19

    Все типы фича-пакета стали public только ради того, чтобы приложение собралось: чего это стоит и что модуль должен выставлять наружу?

    public означает обещание, а не удобство: каждый публичный символ становится API, который придётся поддерживать работающим, а широкая поверхность позволяет любому файлу приложения лезть во внутренности модуля, и это ровно та связность, от которой модуль должен был защищать.

    import SwiftUI // Package.swift: one product, two targets that share internalslet package = Package(  name: "Checkout",  products: [.library(name: "Checkout", targets: ["Checkout"])],…
  20. 20

    Checkout должен открывать редактор адреса из Profile, а Profile должен открывать платёжный экран из Checkout, и SwiftPM отвергает цикл: как его разорвать?

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

    import Foundation // Package.swift: interface targets break the cyclelet package = Package(  name: "Features",  products: […
  21. 21

    Что такое ABI stability и module stability и к чему обязывает SDK включённый BUILD_LIBRARY_FOR_DISTRIBUTION?

    ABI stability позволяет скомпилированному бинарнику работать с Swift-рантаймом ОС; module stability позволяет бинарный фреймворк импортировать более новым компилятором.

    // Framework built with BUILD_LIBRARY_FOR_DISTRIBUTION = YES // Not frozen: you may add a case in 2.0public enum PaymentState: Sendable {  case pending  case settled(id: String)…
  22. 22

    Зачем большие команды генерируют Xcode-проект через Tuist или XcodeGen и как мигрировать проект на 200 таргетов, не замораживая работу над фичами?

    Генераторы заменяют конфликтный project.pbxproj коротким манифестом, который можно ревьюить.

    import ProjectDescription // Project.swift: the target graph as reviewable source, no pbxproj in gitlet project = Project(  name: "App",  settings: .settings(configurations: […