Swift Package Manager (SPM): Полное руководство для разработчиков на Swift
Swift Package Manager (SPM) — это официальный инструмент для управления зависимостями, сборки и распространения кода на языке Swift. Встроенный непосредственно в экосистему Swift, SPM предоставляет единый и стандартизированный способ работы с пакетами, избавляя разработчиков от необходимости использовать сторонние решения, такие как CocoaPods или Carthage. SPM тесно интегрирован с компилятором Swift и Xcode, что делает его идеальным выбором для современных проектов.
Зачем нужен Swift Package Manager? В первую очередь, для управления зависимостями. Любой нетривиальный проект использует сторонние библиотеки: для работы с сетью (Alamofire, URLSession), для парсинга JSON (SwiftyJSON), для работы с базами данных (GRDB, SQLite.swift) и т.д. SPM автоматически загружает, собирает и связывает эти библиотеки с вашим проектом. Во-вторых, SPM позволяет вам самим создавать и публиковать собственные пакеты, делая ваш код многократно используемым. В-третьих, он предоставляет мощный инструмент для сборки проектов, включая поддержку тестов, ресурсов и плагинов.
В отличие от CocoaPods, SPM не требует установки отдельного Ruby-гема или настройки сложного Podfile. В отличие от Carthage, SPM не требует ручного связывания фреймворков. SPM — это "родной" менеджер пакетов, который понимает структуру Swift-проектов "из коробки". Он использует файл Package.swift для описания проекта и его зависимостей, что делает процесс конфигурации прозрачным и предсказуемым.
Установка
Swift Package Manager поставляется вместе с официальным дистрибутивом Swift. Отдельная установка не требуется. Если у вас установлен Swift (начиная с версии 5.0), то SPM уже доступен в вашей системе.
Проверка установки:
swift --version
Если вы видите версию Swift, то SPM готов к работе. Вы также можете проверить наличие самого инструмента:
swift package --version
Установка Swift (если не установлен):
- macOS: Установите Xcode из Mac App Store. Swift и SPM входят в его состав.
- Linux (Ubuntu, CentOS, Amazon Linux): Скачайте и установите дистрибутив с официального сайта swift.org.
- Windows: Скачайте и установите дистрибутив с swift.org.
Быстрый старт
Создадим новый исполняемый проект с помощью SPM. Это минимальный рабочий пример.
1. Создание проекта:
mkdir MySwiftProject
cd MySwiftProject
swift package init --type executable
Эта команда создаст структуру проекта:
Package.swift— файл манифеста.Sources/MySwiftProject/main.swift— точка входа.Tests/— директория для тестов.
2. Содержимое Package.swift:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MySwiftProject",
targets: [
.executableTarget(
name: "MySwiftProject"
)
]
)
3. Содержимое main.swift:
print("Hello, Swift Package Manager!")
4. Сборка и запуск:
swift build
swift run
Вы должны увидеть в консоли: Hello, Swift Package Manager!
Основные методы, функции и классы
Рассмотрим ключевые элементы SPM, с которыми вы будете сталкиваться ежедневно. В основном это API, предоставляемый модулем PackageDescription.
1. Package
Сигнатура: Package(name: String, platforms: [SupportedPlatform]? = nil, products: [Product]? = nil, dependencies: [Package.Dependency]? = nil, targets: [Target]? = nil, swiftLanguageVersions: [SwiftVersion]? = nil, cLanguageStandard: CLanguageStandard? = nil, cxxLanguageStandard: CXXLanguageStandard? = nil)
Что делает: Это главный класс, описывающий весь пакет. Он определяет имя, платформы, продукты (библиотеки/исполняемые файлы), зависимости и цели сборки.
Пример: (см. пример в "Быстрый старт")
2. Target
Сигнатура: Target.target(name: String, dependencies: [Target.Dependency]? = nil, path: String? = nil, exclude: [String]? = nil, sources: [String]? = nil, resources: [Resource]? = nil, publicHeadersPath: String? = nil, cSettings: [CSetting]? = nil, cxxSettings: [CXXSetting]? = nil, swiftSettings: [SwiftSetting]? = nil, linkerSettings: [LinkerSetting]? = nil, plugins: [Target.PluginUsage]? = nil)
Что делает: Определяет цель сборки (модуль). Цель может быть исполняемой (.executableTarget) или библиотекой (.target). Указывает зависимости от других целей и пакетов.
Пример:
targets: [
.target(
name: "MyLibrary",
dependencies: ["SomeDependency"]
),
.executableTarget(
name: "MyApp",
dependencies: ["MyLibrary"]
)
]
3. Product
Сигнатура: Product.library(name: String, type: Product.Library.LibraryType? = nil, targets: [String]) или Product.executable(name: String, targets: [String])
Что делает: Определяет, что "экспортирует" ваш пакет для внешнего мира. Это может быть библиотека (статическая или динамическая) или исполняемый файл.
Пример:
products: [
.library(
name: "MyLibrary",
targets: ["MyLibrary"]
),
.executable(
name: "MyTool",
targets: ["MyTool"]
)
]
4. Package.Dependency
Сигнатура: .package(url: String, from: Version), .package(url: String, exact: Version), .package(url: String, branch: String), .package(url: String, revision: String), .package(path: String) и другие.
Что делает: Описывает зависимость от другого пакета. Указывает источник (URL репозитория, локальный путь) и требования к версии.
Пример:
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"),
.package(path: "../MyLocalPackage")
]
5. Version
Сигнатура: Version(_ string: String) или Version(major: Int, minor: Int, patch: Int)
Что делает: Представляет семантическую версию (SemVer). Используется для указания версий зависимостей.
Пример:
let minVersion: Version = "1.2.3"
let exactVersion = Version(2, 0, 0)
6. SupportedPlatform
Сигнатура: .macOS(_ version: SupportedPlatform.MacOSVersion), .iOS(_ version: SupportedPlatform.iOSVersion), .tvOS(_ version: SupportedPlatform.TVOSVersion), .watchOS(_ version: SupportedPlatform.WatchOSVersion), .visionOS(_ version: SupportedPlatform.VisionOSVersion), .linux(), .macCatalyst(_ version: SupportedPlatform.MacCatalystVersion), .driverKit(_ version: SupportedPlatform.DriverKitVersion)
Что делает: Устанавливает минимальные версии платформ, которые поддерживает ваш пакет.
Пример:
platforms: [
.iOS(.v16),
.macOS(.v13)
]
7. Resource
Сигнатура: .process(_ path: String), .copy(_ path: String)
Что делает: Указывает, как обрабатывать ресурсы (изображения, JSON-файлы, шрифты) в цели. .process применяет стандартные правила (например, для XIB), .copy просто копирует файл.
Пример:
resources: [
.process("Resources"),
.copy("config.json")
]
8. Plugin
Сигнатура: .plugin(name: String, package: String? = nil)
Что делает: Определяет зависимость от плагина (например, для генерации кода или линтинга). Плагины — мощная функция SPM, появившаяся в Swift 5.6.
Пример:
targets: [
.target(
name: "MyTarget",
plugins: [
.plugin(name: "SwiftLintPlugin", package: "SwiftLint")
]
)
]
9. SwiftSetting
Сигнатура: .define(_ name: String, _ value: String? = nil), .unsafeFlags([String]), .enableUpcomingFeature(_ feature: SwiftSetting.UpcomingFeature), .enableExperimentalFeature(_ feature: SwiftSetting.ExperimentalFeature)
Что делает: Позволяет передавать флаги компилятору Swift для конкретной цели. Например, для определения макросов препроцессора или включения экспериментальных возможностей.
Пример:
swiftSettings: [
.define("DEBUG"),
.enableUpcomingFeature("BareSlashRegexLiterals")
]
Реальный практический пример (100-200 строк кода)
Создадим пакет для работы с погодой, который будет получать данные из открытого API (Open-Meteo) и выводить их в консоль. Это покажет, как использовать зависимости, асинхронность и структуру SPM.
1. Создаем проект:
mkdir WeatherKit
cd WeatherKit
swift package init --type executable
2. Редактируем Package.swift:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "WeatherKit",
platforms: [
.macOS(.v10_15) // Для async/await
],
dependencies: [
// Зависимость для работы с сетью (не обязательно, но упростит код)
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
],
targets: [
.executableTarget(
name: "WeatherKit",
dependencies: ["Alamofire"]
)
]
)
3. Редактируем Sources/WeatherKit/main.swift:
import Foundation
import Alamofire
// MARK: - Модели данных
struct WeatherResponse: Codable {
let latitude: Double
let longitude: Double
let currentWeather: CurrentWeather
enum CodingKeys: String, CodingKey {
case latitude, longitude
case currentWeather = "current_weather"
}
}
struct CurrentWeather: Codable {
let temperature: Double
let windspeed: Double
let weathercode: Int
let time: String
}
// MARK: - Сервис погоды
class WeatherService {
private let baseURL = "https://api.open-meteo.com/v1/forecast"
/// Получает текущую погоду для заданных координат
/// - Parameters:
/// - latitude: Широта
/// - longitude: Долгота
/// - Returns: WeatherResponse
func fetchWeather(latitude: Double, longitude: Double) async throws -> WeatherResponse {
let parameters: [String: Any] = [
"latitude": latitude,
"longitude": longitude,
"current_weather": true
]
return try await withCheckedThrowingContinuation { continuation in
AF.request(baseURL, parameters: parameters)
.validate()
.responseDecodable(of: WeatherResponse.self) { response in
switch response.result {
case .success(let weather):
continuation.resume(returning: weather)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
/// Описание погоды на основе кода
static func weatherDescription(from code: Int) -> String {
switch code {
case 0: return "Ясно"
case 1, 2, 3: return "Облачно"
case 45, 48: return "Туман"
case 51, 53, 55: return "Морось"
case 61, 63, 65: return "Дождь"
case 71, 73, 75: return "Снег"
case 95: return "Гроза"
default: return "Неизвестно"
}
}
}
// MARK: - Точка входа
@main
struct WeatherApp {
static func main() async {
let service = WeatherService()
print("🌤️ Погодный терминал")
print("Введите широту (например, 55.7558): ", terminator: "")
guard let latInput = readLine(), let latitude = Double(latInput) else {
print("❌ Ошибка: неверная широта")
return
}
print("Введите долготу (например, 37.6173): ", terminator: "")
guard let lonInput = readLine(), let longitude = Double(lonInput) else {
print("❌ Ошибка: неверная долгота