Какие преимущества и недостатки у Swift Package Manager (SPM) как менеджера зависимостей?

Ответ

Преимущества:

  • Нативная интеграция: Встроен прямо в Xcode и Swift, не требует установки дополнительных инструментов.
  • Декларативная конфигурация: Все зависимости и цели проекта описываются в одном файле Package.swift на языке Swift.
  • Поддержка семантического версионирования: Позволяет точно указывать диапазоны версий (.upToNextMajor, .exact).
  • Управление зависимостями на уровне продукта: Пакет может объявлять несколько библиотечных или исполняемых продуктов.
  • Кроссплатформенность и поддержка серверного Swift: Работает на всех платформах, где есть Swift.

Недостатки и ограничения:

  • Ограниченная поддержка бинарных зависимостей: Хотя бинарные цели (binaryTarget) существуют, их использование менее удобно, чем в CocoaPods (например, для закрытых бинарных SDK).
  • Отсутствие центрального репозитория: Нет аналога CocoaPods Trunk, что усложняет поиск пакетов (хотя интеграция с GitHub и другими Git-хостами хорошая).
  • Сложности с ресурсами для iOS/macOS: Поддержка ресурсов (изображений, xib, storyboard) появилась позже и может требовать дополнительной настройки.
  • Нет встроенного кэширования зависимостей: При отсутствии локального клона пакет загружается заново, в отличие от CocoaPods, который кэширует исходники в ~/.cocoapods.

Пример файла Package.swift:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyLibrary",
    platforms: [
        .iOS(.v15),
        .macOS(.v12)
    ],
    products: [
        .library(
            name: "MyLibrary",
            targets: ["MyLibrary"]),
        .executable(
            name: "MyTool",
            targets: ["MyTool"]),
    ],
    dependencies: [
        // Зависимость из репозитория Git с диапазоном версий
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"),
        // Зависимость из конкретной ветки
        .package(url: "https://github.com/ReactiveX/RxSwift.git", branch: "main"),
        // Зависимость из локального пути (для разработки)
        .package(path: "../MyLocalPackage"),
        // Зависимость с точной версией
        .package(url: "https://github.com/SnapKit/SnapKit.git", exact: "5.6.0")
    ],
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: [
                .product(name: "Alamofire", package: "Alamofire"),
                "RxSwift", // Имя пакета может использоваться как имя зависимости, если оно совпадает
                .product(name: "RxCocoa", package: "RxSwift")
            ],
            resources: [.process("Resources")] // Включение ресурсов
        ),
        .executableTarget(
            name: "MyTool",
            dependencies: ["MyLibrary"]
        ),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary", "SnapKit"] // Зависимость только для тестов
        ),
        // Бинарная цель (XCFramework)
        .binaryTarget(
            name: "MyBinarySDK",
            url: "https://example.com/MyBinarySDK.xcframework.zip",
            checksum: "abc123..."
        )
    ]
)

Ответ 18+ 🔞

А, ну ты про Swift Package Manager, да? Эта штука, которая в Xcode встроена, типа как родной инструмент. Ну, вроде как удобно, не надо никаких CocoaPods отдельно ставить, всё прямо в среде разработки. Но, блядь, как всегда, есть свои подводные камни, о которых тебе никто не расскажет, пока сам не наступишь.

Что там хорошего, говоришь?

  • Встроен прямо в Xcode. Ну да, открыл, добавил пакет по URL — и вроде как работает. Не надо pod install каждый раз бегать делать, что уже приятно.
  • Конфиг на Swift. Файлик Package.swift пишешь на том же языке, на котором и код. Вроде как логично, не надо учить какой-то свой, ёбаный, Ruby DSL, как в тех же Pod'ах.
  • Версии. Тут всё чётко: можешь сказать «дай мне любую версию, но только до следующего мажора», или «только вот эту конкретную, нахуй». Удобно.
  • Не только библиотеки. Можешь в одном пакете и библиотеку описать, и какой-нибудь исполняемый инструмент — типа всё в одном флаконе.
  • Кроссплатформенность. Ну, Swift же. Работает везде, где он есть. Для серверных штук — вообще красота.

А теперь, сука, ложка дёгтя, которая всегда найдётся:

  • Бинарники — боль. Хоть и есть binaryTarget, но попробуй-ка подключи закрытый SDK в виде .xcframework. В CocoaPods это было проще, честно. Тут тебе и checksum надо высчитывать, и если обновили SDK — всё, танцы с бубном.
  • Где искать-то? Нету центральной базы, как CocoaPods Trunk. Сидишь, гадаешь на кофейной гуще: «А есть ли пакет для вот этой задачи на GitHub?» Приходится гуглить, как последний лох.
  • Ресурсы. О, это отдельная песня. Раньше их вообще не было. Сейчас вроде есть, но если тебе надо картинки, xib'ы или storyboard запихнуть — готовься к тому, что может не собраться с первого раза. Настраивай, блядь, resources в таргете.
  • Кэширование — хуёвание. Нет локального кэша, как у Pod'ов в ~/.cocoapods. Удалил пакет из проекта — в следующий раз будет качать заново всё, особенно если интернет тормозной. Веселье.

Вот, смотри, как этот Package.swift выглядит, чтоб ты понимал масштаб:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyLibrary",
    platforms: [
        .iOS(.v15),
        .macOS(.v12)
    ],
    products: [
        .library(
            name: "MyLibrary",
            targets: ["MyLibrary"]),
        .executable(
            name: "MyTool",
            targets: ["MyTool"]),
    ],
    dependencies: [
        // Взял с GitHub, версию любую, но с пятой мажорной
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"),
        // А эту либу тянем прямо с ветки main (живём на грани, да)
        .package(url: "https://github.com/ReactiveX/RxSwift.git", branch: "main"),
        // А это сосед по папке, локальный пакет для тестов
        .package(path: "../MyLocalPackage"),
        // А вот эту — только точную версию, ни шагу в сторону!
        .package(url: "https://github.com/SnapKit/SnapKit.git", exact: "5.6.0")
    ],
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: [
                .product(name: "Alamofire", package: "Alamofire"),
                "RxSwift", // Если имя пакета и продукта совпадают — можно так
                .product(name: "RxCocoa", package: "RxSwift")
            ],
            resources: [.process("Resources")] // Вот тут ресурсы наши, которые могут не загрузиться
        ),
        .executableTarget(
            name: "MyTool",
            dependencies: ["MyLibrary"]
        ),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary", "SnapKit"] // SnapKit только для тестов, например
        ),
        // А вот и бинарник, с которым может быть боль
        .binaryTarget(
            name: "MyBinarySDK",
            url: "https://example.com/MyBinarySDK.xcframework.zip",
            checksum: "abc123..." // Забудешь обновить — проект не соберётся, пиздец
        )
    ]
)

В общем, инструмент в целом адекватный, особенно для чистого Swift. Но если ты привык к Pod'ам и у тебя проект с кучей легаси и бинарных зависимостей — готовься к волшебству и непредсказуемости. Иногда кажется, что он просто издевается.