Первая часть.
Управление зависимостями всегда было тем еще квестом. За свою карьеру я перепробовал множество подходов, от ручного добавления исходников до CocoaPods, Carthage и, наконец, Swift Package Manager. И должен признаться, что с каждым инструментом возникали свои специфические проблемы. Особенно когда речь заходила о кросс-платформенной разработке или условной компиляции разных наборов фич в зависимости от окружения.
Package Traits - новый подход к управлению зависимостями
Swift Package Manager (SPM) значительно упростил управление зависимостями, но до недавнего времени ему не хватало гибкости при работе с пакетами, которые должны вести себя по-разному в разных контекстах. Приходилось городить огороды из директив условной компиляции:
| Swift | 1
2
3
4
5
6
7
| #if os(iOS)
// Код для iOS
#elseif os(macOS)
// Код для macOS
#elseif os(Linux)
// Код для Linux
#endif |
|
И вот в Swift 6.1 появилась, на мой взгляд, революционная фича — Package Traits (трейты пакетов). Эта концепция кардинально меняет подход к управлению зависимостями и условной компиляции.
Что же такое Package Traits? По сути, это механизм, позволяющий определять набор характеристик, которые предоставляет пакет. Эти характеристики могут использоваться для условной компиляции и определения опциональных зависимостей. Проще говоря, вы можете легко указать, какие API и функции доступны в разных средах использования.
Давайте посмотрим, как это работает на практике. Вот как выглядит определение трейтов в файле Package.swift:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Package.swift
let package = Package(
name: "MyNetworkLibrary",
traits: ["Secure", "Logging", "UISupport"],
platforms: [.iOS(.v15), .macOS(.v12)],
products: [
.library(
name: "MyNetworkLibrary",
targets: ["MyNetworkLibrary"]
)
],
dependencies: [
.package(url: "https://github.com/example/crypto.git", from: "1.0.0", traits: ["Secure"]),
.package(url: "https://github.com/example/logger.git", from: "2.0.0", traits: ["Logging"])
],
targets: [
.target(
name: "MyNetworkLibrary",
dependencies: [
.product(name: "Crypto", package: "crypto", traits: ["Secure"]),
.product(name: "Logger", package: "logger", traits: ["Logging"])
]
)
]
) |
|
В этом примере я определил три трейта для своей библиотеки: "Secure", "Logging" и "UISupport". Криптографические возможности подключаются только если активирован трейт "Secure", а логирование — при наличии трейта "Logging".
А вот как можно использовать трейты в самом коде:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @available(trait: Secure)
public func encryptData(_ data: Data) -> Data {
// Реализация шифрования, доступная только при активном трейте Secure
return Crypto.encrypt(data)
}
@available(trait: Logging)
public func logRequest(_ request: URLRequest) {
// Логирование, доступное только при активном трейте Logging
Logger.log(request)
}
@available(trait: UISupport)
public class NetworkIndicator {
// UI-компоненты, доступные только при активном трейте UISupport
} |
|
Когда другой разработчик будет использовать мою библиотеку, он сможет выбрать только те трейты, которые ему нужны:
| Swift | 1
2
3
4
5
6
7
8
| // В проекте, использующем мою библиотеку
dependencies: [
.package(
url: "https://github.com/me/mynetworklibrary.git",
from: "1.0.0",
traits: ["Secure", "Logging"] // UISupport не используется
)
] |
|
Это дает огромные преимущества. Во-первых, код становится чище — вместо множества вложенных директив #if/#endif получаем аккуратные аннотации @available(trait . Во-вторых, снижается размер итогового бинарника, так как компилируется только нужный код. В-третьих, улучшается производительность компиляции, особенно в больших проектах. Но самое главное — Package Traits позволяют создавать по-настоящему модульные библиотеки, компоненты которых можно подключать по отдельности в зависимости от потребностей. Это особенно ценно для кросс-платформенной разработки.
Недавно я разрабатывал библиотеку для обработки данных, которая должна была работать и на iOS, и на Linux-серверах. Раньше мне приходилось поддерживать практически два параллельных кодбейса с кучей условной компиляции. С Package Traits структура упростилась:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Общий код без трейтов
public struct DataProcessor {
public func process(_ data: Data) -> ProcessedData {
// Базовая обработка, доступная везде
}
}
// iOS-специфичный код
@available(trait: iOS)
extension DataProcessor {
public func visualize() -> UIImage {
// Визуализация для iOS
}
}
// Server-специфичный код
@available(trait: Server)
extension DataProcessor {
public func benchmark() -> PerformanceMetrics {
// Измерение производительности для серверов
}
} |
|
А потом в Package.swift я просто определил соответствующие трейты:
| Swift | 1
2
3
4
5
| let package = Package(
name: "DataTools",
traits: ["iOS", "Server"],
// ...остальная конфигурация...
) |
|
Теперь клиенты на iOS подключают пакет с трейтом "iOS", а серверы — с трейтом "Server". Код стал не только чище, но и безопаснее — компилятор не даст использовать функции, недоступные в текущем контексте.
Package Traits также решают давнюю проблему "раздутых" зависимостей. Часто библиотеки тянут за собой кучу дополнительных пакетов, которые не всегда нужны. Теперь с помощью трейтов можно сделать большинство зависимостей опциональными.
Для меня это настоящий game-changer. Раньше приходилось выбирать между созданием множества маленьких специализированных пакетов или одного большого универсального. Теперь можно иметь лучшее из обоих подходов — единый пакет с чётко разделенными компонентами. И что особенно важно, Package Traits работают не только для Swift-кода, но и для нативных библиотек. Например, можно включать разные бинарные зависимости для разных архитектур или операционных систем, что раньше требовало сложных скриптов сборки.
Конечно, как и любая новая технология, Package Traits требуют некоторого переосмысления подходов к организации кода. Но поверьте моему опыту — это та инвестиция времени, которая окупается очень быстро, особенно в долгосрочных проектах.
Визуальная часть в Xcode with Swift подскажите идеи реализации такого таб бара в SWIFT:
1 - что бы были такие вкладки
2 - что бы... Новый язык программирования swift и новый ios sdk Вообщем кто что думает, на сколько сильно этот новый язык отличен от objetive c и перестанет ли... Документация SWIFT Здравствуйте. Не могли бы вы в эту тему накидать документации, особенностей и полезной инфы про... Как установить swift на windows 8? Всем привет, подскажите пожалуйста, как установить swift. ОС виндовс 8. Очень нужно )
Механизм наследования trait'ов и их композиция
Разбираясь с Package Traits глубже, я обнаружил одну из самых мощных их особенностей — механизм наследования и композиции. Именно эта возможность превращает трейты из простых флагов в полноценную систему организации и управления функциональностью пакетов. Попробую объяснить, как это работает и почему это так круто.
Наследование трейтов позволяет одним пакетам "наследовать" трейты от своих зависимостей. Допустим, у нас есть базовый пакет с криптографическими алгоритмами:
| Swift | 1
2
3
4
5
6
| // В CryptoCore/Package.swift
let package = Package(
name: "CryptoCore",
traits: ["AES", "RSA", "ECC"],
// ...
) |
|
Теперь мы создаем другой пакет, который использует этот базовый:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // В SecureMessaging/Package.swift
let package = Package(
name: "SecureMessaging",
traits: ["E2EEncryption"],
dependencies: [
.package(
url: "path/to/CryptoCore",
from: "1.0.0",
traits: ["AES", "ECC"] // Используем только часть трейтов
)
],
// ...
) |
|
В этом примере пакет SecureMessaging автоматически "получает" трейты AES и ECC от CryptoCore. Если какой-то код в SecureMessaging помечен как @available(trait: AES), он будет скомпилирован только если трейт AES активирован в CryptoCore. Это создает элегантную цепочку зависимостей между функциональностями разных пакетов.
Но настоящая сила проявляется при композиции трейтов. В Swift 6.1 можно создавать составные трейты, которые активируются только при наличии нескольких других трейтов:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| // В файле Package.swift
let package = Package(
name: "NetworkingSuite",
traits: [
"HTTP",
"WebSockets",
"Security",
"AdvancedHTTPS": ["HTTP", "Security"], // Составной трейт
"SecureRealtime": ["WebSockets", "Security"] // Еще один составной трейт
],
// ...
) |
|
Здесь мы определяем два составных трейта: "AdvancedHTTPS", который активируется только при наличии трейтов "HTTP" и "Security", и "SecureRealtime", требующий "WebSockets" и "Security". Это позволяет создавать более сложные зависимости между компонентами. В коде мы можем использовать их так:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @available(trait: HTTP)
func performHTTPRequest() {
// Базовый HTTP-запрос
}
@available(trait: Security)
func encryptData(_ data: Data) -> Data {
// Шифрование данных
}
@available(trait: AdvancedHTTPS)
func performSecureRequest() {
let data = prepareRequestData()
let encrypted = encryptData(data) // Доступно, т.к. трейт Security активирован
performHTTPRequest() // Доступно, т.к. трейт HTTP активирован
// Дополнительная логика для HTTPS
} |
|
В моей практике я столкнулся с интересным случаем, когда нам требовалось гибко настраивать функциональность библиотеки для работы с финансовыми данными. У нас были разные требования к безопасности и производительности в зависимости от типа клиента. Используя композицию трейтов, мы создали несколько профилей:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // FinTech/Package.swift
let package = Package(
name: "FinTech",
traits: [
"BasicSecurity",
"AdvancedSecurity",
"HighPerformance",
"EnterpriseSecurity": ["AdvancedSecurity", "AuditLogs", "ComplianceReports"],
"ConsumerFinance": ["BasicSecurity", "HighPerformance", "UserFriendly"],
"InstitutionalFinance": ["EnterpriseSecurity", "HighPerformance"]
],
// ...
) |
|
Такой подход позволил нам адаптировать одну библиотеку для разных типов клиентов, не поддерживая несколько параллельных веток кода.
Что касается разрешения конфликтов, Swift 6.1 предлагает гибкий механизм. Когда несколько зависимостей требуют разных версий одного и того же трейта, пакет-потребитель может явно указать, какая версия должна использоваться:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| dependencies: [
.package(
url: "packageA",
from: "1.0.0",
traits: ["Logging": "v1"]
),
.package(
url: "packageB",
from: "2.0.0",
traits: ["Logging": "v2", override: true]
)
] |
|
Флаг override: true указывает, что версия трейта из packageB должна иметь приоритет над версией из packageA. Это особенно полезно при интеграции сторонних библиотек с разными требованиями.
При проектировании иерархии трейтов я обнаружил полезный паттерн — создание "базовых" и "расширеных" наборов функциональности:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| // DataProcessing/Package.swift
let package = Package(
name: "DataProcessing",
traits: [
"Core", // Базовая функциональность, всегда включена
"Basic": ["Core"], // Минимальный набор для простых случаев
"Standard": ["Basic", "Filtering", "Sorting"], // Стандартный набор
"Advanced": ["Standard", "ML", "StatisticalAnalysis"], // Продвинутый набор
"Enterprise": ["Advanced", "DistributedProcessing", "HighSecurity"] // Полный набор
],
// ...
) |
|
Пользователи могут выбрать уровень функциональности, который им необходим, просто указав соответствующий составной трейт:
| Swift | 1
2
3
4
5
6
7
| dependencies: [
.package(
url: "path/to/DataProcessing",
from: "1.0.0",
traits: ["Standard"] // Включает Core, Basic, Filtering и Sorting
)
] |
|
Что особенно круто в наследовании трейтов — это возможность создавать сложные зависимости между компонентами без жесткой связанности кода. Вместо того чтобы писать условную логику вроде "если доступен компонент A и компонент B, то выполнить X", мы просто помечаем код соответствующим составным трейтом и позволяем системе сборки решать, должен ли он быть включен.
Механизм наследования трейтов также хорошо работает с версионированием. При обновлении пакета можно вводить новые трейты или модифицировать существующие, не нарушая обратную совместимость:
| Swift | 1
2
3
4
5
6
7
8
9
10
| // В версии 1.0.0
traits: ["Basic", "Advanced"]
// В версии 2.0.0
traits: [
"Basic",
"Advanced",
"Experimental", // Новый трейт
"LegacyAdvanced": ["Advanced"] // Для обратной совместимости
] |
|
Клиенты, использующие "Advanced" в версии 1.0.0, могут безболезненно перейти на "LegacyAdvanced" в версии 2.0.0, если новая реализация "Advanced" им не подходит.
В заключение этого раздела хочу отметить, что наследование и композиция трейтов — это те инструменты, которые превращают Package Traits из простой замены условной компиляции в мощный механизм организации модульного кода. С их помощью можно создавать гибкие, масштабируемые архитектуры, которые легко адаптируются под разные требования без дублирования кода и сложных условных конструкций.
Конфигурация и использование в реальных проектах
Теория теорией, но когда дело доходит до реальных проектов, Package Traits требуют определенного подхода к конфигурации и использованию. За последние месяцы я внедрил эту технологию в несколько действующих проектов и хочу поделиться практическими наблюдениями и советами.
Начнем с базовой настройки. Для существующего проекта первый шаг — это модификация файла Package.swift для определения трейтов. Вот пример конфигурации для типичного бизнес-приложения:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // Package.swift
let package = Package(
name: "BusinessApp",
traits: [
"Core", // Базовая функциональность
"UI", // Пользовательский интерфейс
"Networking", // Сетевой слой
"Analytics", // Аналитика
"Reporting", // Отчеты
"AdvancedReporting": ["Reporting", "Analytics"], // Продвинутые отчеты
"FullPack": ["Core", "UI", "Networking", "Analytics", "Reporting"] // Полный набор
],
platforms: [.iOS(.v15), .macOS(.v13)],
products: [
.library(
name: "BusinessCore",
targets: ["BusinessCore"],
traits: ["Core"] // Только базовая функциональность
),
.library(
name: "BusinessComplete",
targets: ["BusinessComplete"],
traits: ["FullPack"] // Полный функционал
)
],
dependencies: [
.package(url: "https://example.com/networking.git", from: "1.0.0", traits: ["Networking"]),
.package(url: "https://example.com/analytics.git", from: "2.0.0", traits: ["Analytics"])
],
targets: [
.target(
name: "BusinessCore",
dependencies: []
),
.target(
name: "BusinessComplete",
dependencies: [
"BusinessCore",
.product(name: "Networking", package: "networking", traits: ["Networking"]),
.product(name: "Analytics", package: "analytics", traits: ["Analytics"])
]
),
.testTarget(
name: "BusinessTests",
dependencies: ["BusinessComplete"]
)
]
) |
|
В этой конфигурации я определил несколько базовых трейтов и два составных — "AdvancedReporting" и "FullPack". Обратите внимание, что я создал два разных продукта: облегченный "BusinessCore" только с базовой функциональностью и полнофункциональный "BusinessComplete".
После настройки Package.swift следующий шаг — маркировка кода с помощью аннотаций @available(trait:). Вот как это выглядит на практике:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // Базовая функциональность, доступна всегда
public struct User {
public let id: UUID
public let name: String
}
// Функции для работы с сетью, доступны только при активном трейте Networking
@available(trait: Networking)
public extension User {
func sync() async throws {
// Синхронизация с сервером
}
static func fetch(id: UUID) async throws -> User {
// Загрузка пользователя с сервера
}
}
// Аналитика, доступна только при активном трейте Analytics
@available(trait: Analytics)
public extension User {
func trackActivity(_ activity: UserActivity) {
// Отправка данных в аналитику
}
}
// Функционал отчетов, требует трейта Reporting
@available(trait: Reporting)
public func generateUserReport(for user: User) -> Report {
// Генерация базового отчета
return Report(user: user)
}
// Продвинутая отчетность, требует составного трейта AdvancedReporting
@available(trait: AdvancedReporting)
public func generateDetailedUserReport(for user: User) -> DetailedReport {
// Используем базовый отчет
let baseReport = generateUserReport(for: user)
// Добавляем аналитические данные
user.trackActivity(.reportGenerated)
// Создаем расширенный отчет
return DetailedReport(baseReport: baseReport, analytics: fetchAnalytics(for: user))
} |
|
Важное практическое замечание: маркируйте трейтами не только публичный API, но и внутренние детали реализации. Это позволит компилятору исключить ненужный код и избежать ошибок, связаных с отсутствием зависимостей.
Конфигурация и использование в реальных проектах
Начнем с базовой конфигурации в файле Package.swift. Самый простой способ определить трейты выглядит так:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Package.swift
let package = Package(
name: "AnalyticsService",
traits: ["Basic", "Premium", "Enterprise"],
platforms: [.iOS(.v15), .macOS(.v12)],
products: [
.library(
name: "AnalyticsService",
targets: ["AnalyticsService"]
)
],
// ...
) |
|
Но в реальной жизни всё обычно сложнее. Мне недавно пришлось переработать большую библиотеку аналитики, которая имела разные возможности для разных клиентов. Раньше у нас было три отдельные версии, которые приходилось синхронизировать вручную. С Package Traits решение выглядит гораздо элегантнее:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // Package.swift
let package = Package(
name: "AnalyticsService",
traits: [
"Core", // Базовый функционал
"SessionTracking", // Отслеживание сессий
"Heatmaps", // Тепловые карты
"NetworkMonitoring", // Мониторинг сети
"CrashReporting", // Отчеты о крешах
"UserJourneyAnalytics", // Аналитика пользовательского пути
// Составные трейты для разных уровней сервиса
"Basic": ["Core", "SessionTracking"],
"Standard": ["Basic", "Heatmaps", "CrashReporting"],
"Premium": ["Standard", "NetworkMonitoring", "UserJourneyAnalytics"]
],
platforms: [.iOS(.v15), .macOS(.v12)],
products: [
.library(
name: "AnalyticsService",
targets: ["AnalyticsService"]
)
],
dependencies: [
.package(
url: "https://github.com/example/networking.git",
from: "1.0.0",
traits: ["Secure", "Compression"] // Необходимые трейты зависимости
),
.package(
url: "https://github.com/example/storage.git",
from: "2.0.0",
traits: ["SQLite"] // Только SQLite хранилище, без других опций
)
],
targets: [
.target(
name: "AnalyticsService",
dependencies: [
.product(name: "Networking", package: "networking"),
.product(name: "Storage", package: "storage")
]
),
.testTarget(
name: "AnalyticsServiceTests",
dependencies: ["AnalyticsService"]
)
]
) |
|
В самом коде библиотеки я использую трейты для разделения функциональности:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // Core functionality - всегда доступно
public class AnalyticsManager {
public init() {
// Инициализация
}
public func trackEvent(_ name: String, parameters: [String: Any]? = nil) {
// Базовое отслеживание событий
}
}
// Session tracking - доступно при активации трейта
@available(trait: SessionTracking)
extension AnalyticsManager {
public func startSession() {
// Начало сессии
}
public func endSession() {
// Завершение сессии
}
}
// Heatmaps - доступно при активации трейта
@available(trait: Heatmaps)
extension AnalyticsManager {
public func enableHeatmapCollection() {
// Включение сбора данных для тепловых карт
}
public func getHeatmapData() -> HeatmapData {
// Получение данных тепловой карты
return HeatmapData()
}
}
// И так далее для других компонентов... |
|
Теперь клиенты могут выбирать нужный уровень функциональности, просто указав соответствующий трейт:
| Swift | 1
2
3
4
5
6
7
8
| // В проекте клиента
dependencies: [
.package(
url: "path/to/AnalyticsService",
from: "1.0.0",
traits: ["Standard"] // Получаем Core, SessionTracking, Heatmaps и CrashReporting
)
] |
|
Интересный вопрос, который у меня возник при внедрении Package Traits — как эффективно тестировать код, зависящий от наличия определенных трейтов? Я разработал подход с использованием отдельных тестовых таргетов для разных комбинаций трейтов:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Package.swift (для тестирования)
targets: [
// Основной тестовый таргет
.testTarget(
name: "CoreTests",
dependencies: ["AnalyticsService"]
),
// Тесты для Standard функциональности
.testTarget(
name: "StandardFeaturesTests",
dependencies: ["AnalyticsService"],
traits: ["Standard"]
),
// Тесты для Premium функциональности
.testTarget(
name: "PremiumFeaturesTests",
dependencies: ["AnalyticsService"],
traits: ["Premium"]
)
] |
|
Это позволяет изолировать тесты и гарантировать, что функциональность корректно работает при разных конфигурациях трейтов.
Еще один практический аспект — обратная совместимость при обновлении библиотеки. Когда мы выпускаем новую версию с измененими трейтами, важно обеспечить плавный переход для существующих клиентов. Я выработал следующий подход:
| Swift | 1
2
3
4
5
6
7
8
9
| // В версии 1.0.0
traits: ["Basic", "Advanced"]
// В версии 2.0.0
traits: [
"Basic",
"Enhanced", // Новая версия Advanced с улучшениями
"Legacy.Advanced": ["Enhanced"] // Алиас для обратной совместимости
] |
|
Таким образом, клиенты, использующие трейт "Advanced", могут продолжать использовать его, но фактически получат новую функциональность "Enhanced".
При разработке внутренних библиотек в нашей компании я столкнулся с проблемой: как организовать совместное использование трейтов между разными пакетами, чтобы избежать дублирования определений? Решение оказалось не очевидным, но эффективным — создание "метапакета" с общими определениями трейтов:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // CommonTraits/Package.swift
let package = Package(
name: "CommonTraits",
traits: [
"UI.Basic", "UI.Advanced",
"Networking.Basic", "Networking.Secure",
"Storage.SQLite", "Storage.CoreData"
],
products: [
.library(name: "CommonTraits", targets: ["CommonTraits"])
],
targets: [
.target(name: "CommonTraits", sources: ["empty.swift"])
]
) |
|
Файл empty.swift содержит минимальный код, нужный только для корректной компиляции:
| Swift | 1
2
| // empty.swift
public enum CommonTraits {} |
|
Теперь другие пакеты могут импортировать эти трейты:
| Swift | 1
2
3
4
5
6
7
8
| // NetworkingPackage/Package.swift
dependencies: [
.package(url: "path/to/CommonTraits", from: "1.0.0")
],
traits: [
"Basic": ["UI.Basic"], // Используем трейт из CommonTraits
"Secure": ["Networking.Secure"] // Используем еще один трейт
] |
|
Это обеспечивает единообразие трейтов во всей экосистеме пакетов и упрощает поддержку.
Использование трейтов в CI/CD пайплайнах
Отдельная история — интеграция Package Traits с системами непрерывной интеграции. В наших проектах мы активно используем GitHub Actions, и я разработал подход для автоматической проверки разных конфигураций трейтов:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # .github/workflows/test.yml
name: Run Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test-basic:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test Basic Configuration
run: swift test --traits Basic
test-standard:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test Standard Configuration
run: swift test --traits Standard
test-premium:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test Premium Configuration
run: swift test --traits Premium |
|
Такой подход позволяет гарантировать, что все комбинации трейтов корректно работают после каждого изменения кода.
При разработке кроссплатформенных приложений Package Traits стали для меня настоящим спасением. Например, в одном проекте нам требовалось поддерживать iOS, macOS и даже Windows (через SwiftWasm). Раньше это была настоящая головная боль с множеством условных компиляций, а теперь структура упростилась:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // Общий код
public struct DataProcessor {
public func process(_ data: Data) -> ProcessedData {
// Общая логика
}
}
// iOS-специфичный код
@available(trait: iOS)
extension DataProcessor {
public func visualizeOnDevice() {
// iOS-реализация
}
}
// macOS-специфичный код
@available(trait: macOS)
extension DataProcessor {
public func visualizeOnDevice() {
// macOS-реализация
}
}
// Windows-специфичный код
@available(trait: Windows)
extension DataProcessor {
public func visualizeOnDevice() {
// Windows-реализация через WebAssembly
}
} |
|
Обратите внимание, что метод visualizeOnDevice() имеет разные реализации для разных платформ, но интерфейс остается одним и тем же. Это значительно упрощает код, использующий DataProcessor.
Интеграция с GitHub Actions и автоматизация CI/CD
После внедрения Package Traits в несколько проектов я быстро осознал, что их настоящая мощь раскрывается только при правильной интеграции с системами непрерывной интеграции. Особенно это касается проектов, где используются разные конфигурации трейтов для разных сред или клиентов.
Мой основной инструмент для CI/CD последние пару лет — GitHub Actions. И хотя я уже кратко упоминал интеграцию трейтов с системами CI в предыдущем разделе, хочу поделиться более детальным опытом построения действительно эффективных пайплайнов. Начнем с базовой матрицы для тестирования различных конфигураций трейтов:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # .github/workflows/build-and-test.yml
name: Build and Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
trait-set: [Basic, Standard, Premium]
steps:
- uses: actions/checkout@v3
- name: Setup Swift
uses: swift-actions/setup-swift@v1
with:
swift-version: "6.1"
- name: Build and Test
run: swift test --traits ${{ matrix.trait-set }} |
|
Этот простой пайплайн уже дает нам матрицу из 6 различных комбинаций (3 набора трейтов × 2 операционные системы). Но на реальных проектах все обычно сложнее.
В одном из моих недавних проектов нам требовалось не только тестировать разные наборы трейтов, но и собирать бинарные артефакты с разными конфигурациями. Решение выглядело примерно так:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| # .github/workflows/release.yml
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
build:
name: Build and Package
runs-on: macos-latest
strategy:
matrix:
config: [
{name: "Basic", traits: "Basic", suffix: "basic"},
{name: "Standard", traits: "Standard", suffix: "standard"},
{name: "Premium", traits: "Premium,Analytics", suffix: "premium"}
]
steps:
- uses: actions/checkout@v3
- name: Setup Swift
uses: swift-actions/setup-swift@v1
with:
swift-version: "6.1"
- name: Build ${{ matrix.config.name }} Package
run: |
swift build --traits ${{ matrix.config.traits }} -c release
mv .build/release/MyApp .build/release/MyApp-${{ matrix.config.suffix }}
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: myapp-${{ matrix.config.suffix }}
path: .build/release/MyApp-${{ matrix.config.suffix }}
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
path: ./artifacts
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
./artifacts/myapp-basic/MyApp-basic
./artifacts/myapp-standard/MyApp-standard
./artifacts/myapp-premium/MyApp-premium |
|
Такой подход позволяет автоматически создавать релизы с разными вариантами приложения в зависимости от включенных трейтов.
Но самое интересное начинается, когда вы интегрируете Package Traits с другими этапами CI/CD. Например, в одном проекте мы использовали трейты для условного запуска различных наборов тестов, включая интеграционные и UI-тесты:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # .github/workflows/comprehensive-testing.yml
name: Comprehensive Testing
on:
schedule:
- cron: '0 2 * * *' # Запуск каждый день в 2:00 UTC
jobs:
unit-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Run Unit Tests
run: swift test --traits Basic
integration-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Run Integration Tests
run: swift test --traits "Standard,TestInfrastructure"
ui-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Simulator
run: xcrun simctl create "iPhone 14" "iPhone 14" "iOS16.2"
- name: Run UI Tests
run: xcodebuild test -scheme MyApp -destination "platform=iOS Simulator,name=iPhone 14" -traits "UITesting" |
|
В этом примере мы используем специальные трейты для тестирования, которые активируют различные наборы тестов. Это позволяет оптимизировать процесс CI/CD — например, быстрые юнит-тесты запускаются на каждом PR, а длительные UI-тесты — только ночью.
Еще один мощный сценарий — автоматизация сборки и публикации документации в зависимости от трейтов. Я внедрил его в один из наших опенсорсных проектов:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # .github/workflows/documentation.yml
name: Generate Documentation
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
generate-docs:
runs-on: macos-latest
strategy:
matrix:
trait-set: [
{traits: "Basic", folder: "basic"},
{traits: "Standard", folder: "standard"},
{traits: "Premium", folder: "premium"}
]
steps:
- uses: actions/checkout@v3
- name: Setup Swift
uses: swift-actions/setup-swift@v1
- name: Install swift-doc
run: brew install swiftdocorg/tap/swift-doc
- name: Generate Documentation
run: |
swift build --traits ${{ matrix.trait-set.traits }}
swift-doc generate . --module-name MyLibrary --traits ${{ matrix.trait-set.traits }} --output docs/${{ matrix.trait-set.folder }}
- name: Upload Documentation
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: docs
branch: gh-pages |
|
Таким образом мы генерируем отдельные наборы документации для разных уровней API в зависимости от активированных трейтов. Пользователи могут видеть только те методы и классы, которые доступны в выбраной ими конфигурации.
Интересным вызовом для меня стала интеграция Package Traits с мониторингом производительности. Я разработал специальный воркфлоу для отслеживания изменений производительности при различных конфигурациях трейтов:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| # .github/workflows/performance.yml
name: Performance Monitoring
on:
push:
branches: [ main ]
jobs:
benchmark:
runs-on: macos-latest
strategy:
matrix:
trait-set: [Basic, Standard, Premium]
steps:
- uses: actions/checkout@v3
- name: Run Benchmarks
run: swift run --traits ${{ matrix.trait-set }} Benchmarks
- name: Parse Results
id: parse
run: |
RESULT=$(cat benchmark-results.json)
echo "::set-output name=result::$RESULT"
- name: Store Results
uses: benchmark-action/github-action-benchmark@v1
with:
name: Swift Benchmark (${{ matrix.trait-set }})
tool: 'customBiggerIsBetter'
output-file-path: benchmark-results.json
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true |
|
Этот пайплайн запускает бенчмарки для каждой конфигурации трейтов и сохраняет результаты в виде графиков на GitHub Pages. Это позволяет нам отслеживать, как различные комбинации трейтов влияют на производительность нашей библиотеки.
Но не все так гладко. В процессе внедрения этих пайплайнов я столкнулся с несколькими проблемами. Одна из них — ограничения GitHub Actions по времени выполнения. Если у вас много различных комбинаций трейтов, тестирование всех вариантов может занимать слишком много времени. Решение, которое я нашел — стратегическое разделение тестов на критические и некритические. Критические запускаются на каждом PR, а полная матрица комбинаций — только при мердже в основную ветку:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # .github/workflows/smart-testing.yml
name: Smart Testing
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
determine-tests:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Только базовые тесты для PR
echo "matrix={"trait-set":["Basic"]}" >> $GITHUB_OUTPUT
else
# Полная матрица для мерджа в main
echo "matrix={"trait-set":["Basic","Standard","Premium"]}" >> $GITHUB_OUTPUT
fi
test:
needs: determine-tests
runs-on: macos-latest
strategy:
matrix: ${{ fromJson(needs.determine-tests.outputs.matrix) }}
steps:
- uses: actions/checkout@v3
- name: Run Tests
run: swift test --traits ${{ matrix.trait-set }} |
|
Еще одна особенность интеграции Package Traits с CI/CD — это управление зависимостями. Если у вас есть несколько пакетов с трейтами, которые зависят друг от друга, настройка правильного пайплайна может быть нетривиальной задачей. Я нашел элегантное решение с использованием монорепозитория и составных воркфлоу:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| # .github/workflows/monorepo-ci.yml
name: Monorepo CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
changes:
runs-on: ubuntu-latest
outputs:
core: ${{ steps.filter.outputs.core }}
network: ${{ steps.filter.outputs.network }}
ui: ${{ steps.filter.outputs.ui }}
app: ${{ steps.filter.outputs.app }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
core:
- 'Packages/Core/[B]'
network:
- 'Packages/Network/[/B]'
ui:
- 'Packages/UI/[B]'
app:
- 'App/[/B]'
test-core:
needs: changes
if: needs.changes.outputs.core == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test Core
run: cd Packages/Core && swift test --traits "Base"
test-network:
needs: [changes, test-core]
if: needs.changes.outputs.network == 'true' || needs.changes.outputs.core == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test Network
run: cd Packages/Network && swift test --traits "Base,Secure"
test-ui:
needs: [changes, test-core]
if: needs.changes.outputs.ui == 'true' || needs.changes.outputs.core == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test UI
run: cd Packages/UI && swift test --traits "Base,Animations"
test-app:
needs: [changes, test-network, test-ui]
if: always() && (needs.changes.outputs.app == 'true' || needs.changes.outputs.network == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.core == 'true')
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Test App
run: swift test --traits "Base,Secure,Animations" |
|
Этот пайплайн анализирует, какие пакеты были изменены, и запускает тесты только для них и их зависимостей, что значительно ускоряет процесс CI/CD.
Автоматизация развертывания в разных средах
Отдельная тема — использование Package Traits для автоматизации развертывания приложений в разных средах (разработка, тестирование, продакшн). В одном из проектов я реализовал следующий подход:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # .github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'dev'
type: choice
options:
- dev
- test
- prod
jobs:
deploy:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup environment-specific traits
id: traits
run: |
if [[ "${{ github.event.inputs.environment }}" == "dev" ]]; then
echo "traits=Development,Logging,MockServices" >> $GITHUB_OUTPUT
elif [[ "${{ github.event.inputs.environment }}" == "test" ]]; then
echo "traits=TestEnvironment,Logging,Analytics" >> $GITHUB_OUTPUT
else
echo "traits=Production,Analytics" >> $GITHUB_OUTPUT
fi
- name: Build for ${{ github.event.inputs.environment }}
run: swift build --traits ${{ steps.traits.outputs.traits }} -c release
- name: Deploy to ${{ github.event.inputs.environment }}
run: ./deploy.sh ${{ github.event.inputs.environment }} |
|
Такой подход позволяет собирать разные варианты приложения для разных сред, активируя соответствующие трейты. Например, в dev-среде мы включаем подробное логирование и мок-сервисы, а в продакшене — только необходимую аналитику.
На одном из наших проектов мы пошли еще дальше и интегрировали Package Traits с Feature Flags. Это позволило нам иметь общую кодовую базу, но активировать разные функции для разных клиентов или сред:
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
| // В коде приложения
struct FeatureManager {
static func isEnabled(_ feature: Feature) -> Bool {
#if trait(Premium)
// В Premium-версии все функции доступны
return true
#else
// В других версиях проверяем на сервере
return checkFeatureFlagOnServer(feature)
#endif
}
} |
|
Соответствующий CI/CD пайплайн выглядел примерно так:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| # .github/workflows/client-specific-build.yml
name: Client-Specific Build
on:
workflow_dispatch:
inputs:
client:
description: 'Client identifier'
required: true
type: string
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Determine client traits
id: client-traits
run: |
CLIENT_CONFIG=$(cat clients/${{ github.event.inputs.client }}.json)
TRAITS=$(echo $CLIENT_CONFIG | jq -r '.traits | join(",")')
echo "traits=$TRAITS" >> $GITHUB_OUTPUT
- name: Build for ${{ github.event.inputs.client }}
run: swift build --traits ${{ steps.client-traits.outputs.traits }} -c release
- name: Package application
run: ./package.sh ${{ github.event.inputs.client }}
- name: Upload to client portal
run: ./upload.sh ${{ github.event.inputs.client }} |
|
Здесь для каждого клиента мы храним конфигурационный файл JSON с указанием требуемых трейтов, и CI/CD система автоматически собирает и развертывает приложение с нужной функциональностью.
Подводя итоги, могу сказать, что интеграция Package Traits с системами CI/CD открывает огромные возможности для автоматизации процессов разработки, тестирования и развертывания приложений. Это позволяет создавать более гибкие, надежные и эффективные пайплайны, которые могут адаптироваться к различным требованиям и средам. В следующем разделе мы рассмотрим, как Package Traits влияют на архитектуру крупных проектов и какие паттерны проектирования особенно хорошо работают с этой технологией.
Необходимость Swift для не очень опытного разработчика Всем привет!
Возможно, мой вопрос покажется надуманным, но меня это постоянно пилит, хочу... Восклицательный знак в Swift Всем привет!
Начал опыты со Swift, и тут же столкнулся с модификаторами ? и ! (назову их так)... Аналог [object class] в Swift Всем добрый день.
Наконец-то дошли руки до знакомства с RESTKit, и решил сразу попробовать это... Потоки в Swift В общем, решил поковырять свифт на выходных и выяснил, что не могу нормально создавать потоки. То... Массив Swift Есть кусок кода Swift в Xcode:
var pageData = NSArray()
override init() {
... Swift compiler error Command failed due to signal: Bus error: 10 Mavericks 10.9.5, VMWare 10.0.3, xCode 6.0.1 (вообще перепробовал все выпуски, в том числе и 6.1... Учить ли Objective-C новичку или сразу Swift? Хочу начать изучать программирование под iOS есть ли смысл учить старый Objective-C или можно сразу... 2D Движок для написания игры на SWIFT Доброго времени суток, программисты!
Проблема тут у меня. Подскажите какой оптимальный 2D движок... События в Cocoa Swift У меня нет совершенно никакого опыта в написании приложений под мак или айфон, но сейчас... Цикл for / массив в языке Swift Я толко начала изучать Swift и при написания простого приложения "Генератор случайных чисел"... VK SDK swift Подскажите пожалуйста, как можно подключить VK SDK к проекту на swift. Легко ли это вообще сделать... Input/output в swift Начал изучать swift и столкнулся с проблемой ввода значений с клавиатуры. Много чего облазил, но...
|