
Swift Package Manager сегодня является стандартным инструментом для модульной архитектуры iOS-проектов. Он позволяет разделять код на независимые модули,
ускорять сборку и явно описывать зависимости. Однако по мере роста проекта файл Package.swift часто превращается в длинный список строковых зависимостей:
.target( name: "SomeFeature", dependencies: [ "Core", "UI", "Resources" ] )
Меня всегда раздражала одна особенность Package.swift:
мы описываем зависимости, но не описываем архитектуру, из-за этого:
переименование модулей усложняется;
архитектурные правила не проверяются компилятором;
количество повторяющегося кода быстро растёт.
В этой статье вместо того, чтобы рассматривать Package.swift как простой конфигурационный файл, превратим его в типобезопасный DSL для модульной архитектуры, где:
модули описываются через enum;
фичи генерируются декларативно;
архитектурные правила фиксируются в коде.
В итоге объявление зависимостей будет выглядеть так:
Libraries.allCases.map { $0.info.buildDependency() } Local.Core.target() Local.UI.target(deps: [.module(.Core)]) Local.DI.target(deps: [.module(.Core)]) Local.Resources.target() featureTargets(module: { .SomeFeature($0)})
Погнали!
Ключевая особенность SwiftPM в том, что манифесты — это обычные Swift-файлы. Это значит, что мы можем использовать возможности языка для описания архитектуры, пусть и с некоторыми ограничениями.
и при попытке сделать проект с многомодульной архитектурой я получал примерно следующее:
.target( name: "NewsPresentation", dependencies: [ "NewsDomain", "Core", "UI", "Resources" ] ) .target( name: "NewsDomain", dependencies: [ "NewsData", "Core" ] ) .target( name: "NewsData", dependencies: [ "Core" ] )
Если в проекте пять фич — это уже 15 объявлений target. Если десять — то 30.
Используя DSL, ту же архитектуру можно выразить одной строкой:
featureTargets(module: { .News($0)})
при этом будут сгенерированы следующие слои:
News_Presentation
News_Domain
News_Data
с уже правильно настроенным графом зависимостей

Проектируем DSL
Основная идея очень простая: большинство модульных архитектур следуют предсказуемым шаблонам. Вместо того чтобы повторять эти шаблоны в Package.swift, мы можем описать их прямо на Swift.
Объявление сторонних библиотек:
enum RemotePackages: CaseIterable { case Alamofire var spec: RemotePackageSpec { switch self { case .Alamofire: return .init( "https://github.com/Alamofire/Alamofire.git", packageName: "Alamofire", version: "5.10.0" ) } } }
Теперь список зависимостей можно сгенерировать декларативно:
RemotePackages.allCases.map { $0.info.buildDependency() }
Объявление локальных модулей:
enum Local { case Core case UI case DI case Resources case Networking }
После этого объявление target выглядит так:
Local.Core.target() Local.UI.target(deps: [.module(.Core)]) Local.DI.target(deps: [.module(.Core)])
Объявление feature-модулей
enum Local { case Core case UI case DI case Resources case Networking case News(_ layer: FeatureLayer) // обьявляем фича модуль }
Фича состоит из нескольких слоев:
enum FeatureLayer: String { case Presentation case Domain case Data }
Эти слои будут использоваться для автоматической генерации target-модулей.
Настоящая же ценность DSL — в кодировании правил зависимостей между слоями:
func featureTargets( module: ( _ layer: FeatureLayer) -> Local, presentationExtra: [Target.Dependency] = [], domainExtra: [Target.Dependency] = [], dataExtra: [Target.Dependency] = [] ) -> [Target] { let presentation = module(.Presentation) let domain = module(.Domain) let data = module(.Data) return [ presentation.target(deps: [ .module(domain.name), .module(.Core), .module(.UI), .module(.Resources) ] + presentationExtra), domain.target(deps: [ .module(data.name), .module(.Core) ] + domainExtra), data.target(deps: [ .module(.Core), .module(.Networking) ] + dataExtra) ] }
И теперь объявление feature-модуля выглядит так:
featureTargets(module: { .Authorisation($0)} ]) featureTargets(module: { .News($0)} )
Настройка FeatureLayer
Еще одно преимущество такого подхода — структура слоев полностью настраиваемая. Команда может выбирать ее в зависимости от архитектуры проекта.
Например, можно разделить фичу на API и реализацию:
FeatureApi
FeatureImpl
Или использовать более детализированную структуру, например VIPER:
View
Presenter
Interactor
Router
DataStore
Важно не количество слоев, а правила зависимостей между ними.
Именно эти правила DSL позволяет зафиксировать в коде.
Полный текст package.swift
// swift-tools-version: 5.9 import PackageDescription import Foundation // MARK: - Declarations enum ProjectPaths { static let sources = "Sources" } // MARK: Local Modules enum Local { case Core case UI case DI case Resources case Networking case Router(_ layer: FeatureImplLayer) case MainScreen(_ layer: FeatureLayer) case DetailScreen(_ layer: FeatureLayer) } // MARK: Remote Packages enum RemotePackages: CaseIterable { case Alamofire var spec: RemotePackageSpec { switch self { case .Alamofire: return .init( "https://github.com/Alamofire/Alamofire.git", packageName: "Alamofire", version: "5.8.0" ) } } } // MARK: Feature Layering System (Optional) enum FeatureLayer: String { case Presentation, Domain, Data } enum FeatureImplLayer: String { case Impl, Api } // MARK: - Package Formation let packageName = "DemoApp" let package = buildPackage( name: packageName, defaultLocalization: "en", platforms: [.iOS(.v15)] ) { [ Local.Router(.Impl).product(), Local.Router(.Api).product(), Local.Core.product() ] } dependencies: { RemotePackages.allCases.map { $0.spec.buildDependency() } } targets: { // Base modules Local.Networking.target(deps: [.module(.Core), .library(.Alamofire)]) Local.UI.target(deps: [.module(.Core)]) Local.DI.target(deps: [.module(.Core)]) Local.Core.target() Local.Resources.target(resources: [ .process("Resources.xcassets") ]) featureTargets(module: { .Router($0) }, implementationExtra: [ .module(.MainScreen(.Presentation)), .module(.DetailScreen(.Presentation)) ]) featureTargets(module: { .MainScreen($0)}, presentationExtra: [ .module(Local.DI)] ) featureTargets(module: { .DetailScreen($0)}, presentationExtra: [ .module(Local.DI)] ) } // MARK: - Feature configuration func featureTargets( module: ( _ layer: FeatureLayer) -> Local, presentationExtra: [Target.Dependency] = [], domainExtra: [Target.Dependency] = [], dataExtra: [Target.Dependency] = [] ) -> [Target] { let presentation = module(.Presentation) let domain = module(.Domain) let data = module(.Data) return [ presentation.target(deps: [ .module(domain.name), .module(.Core), .module(.UI), .module(.Resources) ] + presentationExtra), domain.target(deps: [ .module(data.name), .module(.Core) ] + domainExtra), data.target(deps: [ .module(.Core), .module(.Networking) ] + dataExtra) ] } func featureTargets( module: ( _ layer: FeatureImplLayer) -> Local, implementationExtra: [Target.Dependency] = [], apiExtra: [Target.Dependency] = [] ) -> [Target] { let implementation = module(.Impl) let api = module(.Api) return [ implementation.target(deps: [ .module(api), .module(.Core) ] + implementationExtra), api.target(deps: apiExtra) ] } // DSL PART // MARK: - Helpers превращает enum Local в рабочее описание модуля extension Local { var name: String { let parsed = parsedDescription if let layer = parsed.layer { return "\(parsed.base)_\(layer)" } return parsed.base } private var path: String { let parsed = parsedDescription if let layer = parsed.layer { return "\(ProjectPaths.sources)/Features/\(parsed.base)/\(layer)" } return "\(ProjectPaths.sources)/\(parsed.base)" } private func module(_ resources: [Resource]?) -> TargetSpec { return TargetSpec(name: name, path: path, resources: resources) } private var module: TargetSpec { TargetSpec(name: name, path: path) } func target(deps: [Target.Dependency] = [], resources: [Resource]? = nil) -> Target { module(resources).target(deps: deps) } func product() -> Product { module.product() } } //Упрощает объявление зависимостей extension Target.Dependency { static func module(_ m: Local) -> Target.Dependency { .target(name: m.name) } static func module(_ name: String) -> Target.Dependency { .target(name: name) } static func library(_ lib: RemotePackages) -> Target.Dependency { .product(name: lib.spec.productName, package: lib.spec.packageName) } } // MARK: - DSL Core // Позволяет декларативно собирать список Target @resultBuilder enum TargetsBuilder { static func buildBlock(_ parts: [Target]...) -> [Target] { parts.flatMap { $0 } } static func buildExpression(_ t: Target) -> [Target] { [t] } static func buildExpression(_ ts: [Target]) -> [Target] { ts } } // Позволяет декларативно собирать список Product @resultBuilder enum ProductsBuilder { static func buildBlock(_ parts: [Product]...) -> [Product] { parts.flatMap { $0 } } static func buildExpression(_ p: Product) -> [Product] { [p] } static func buildExpression(_ ps: [Product]) -> [Product] { ps } } // Позволяет декларативно собирать список Package.Dependency. @resultBuilder enum DependenciesBuilder { static func buildBlock(_ parts: [Package.Dependency]...) -> [Package.Dependency] { parts.flatMap { $0 } } static func buildExpression(_ d: Package.Dependency) -> [Package.Dependency] { [d] } static func buildExpression(_ ds: [Package.Dependency]) -> [Package.Dependency] { ds } } // Обёртка над Package, чтобы собирать package через твой DSL func buildPackage( name: String, defaultLocalization: LanguageTag? = nil, platforms: [SupportedPlatform] = [], @ProductsBuilder products: () -> [Product], @DependenciesBuilder dependencies: () -> [Package.Dependency], @TargetsBuilder targets: () -> [Target] ) -> Package { PackageSpec( name: name, defaultLocalization: defaultLocalization, platforms: platforms, products: products(), dependencies: dependencies(), targets: targets() ).build() } // MARK: - Specs // Промежуточная модель для сборки Package struct PackageSpec { var name: String var defaultLocalization: LanguageTag? var platforms: [SupportedPlatform] = [] var products: [Product] = [] var dependencies: [Package.Dependency] = [] var targets: [Target] = [] func build() -> Package { Package( name: name, defaultLocalization: defaultLocalization, platforms: platforms, products: products, dependencies: dependencies, targets: targets ) } } // Модель для описания внешнего пакета struct RemotePackageSpec { let url: String let packageName: String let productName: String let version: Version init(_ url: String, packageName: String, productName: String? = nil, version: Version) { self.url = url self.packageName = packageName self.productName = productName ?? packageName self.version = version } func buildDependency() -> Package.Dependency { .package(url: url, from: version) } } // Модель для описания target struct TargetSpec { private let name: String private let path: String private let resources: [Resource]? init(name: String, path: String, resources: [Resource]? = nil) { self.name = name self.path = path self.resources = resources } func target(deps: [Target.Dependency] = []) -> Target { .target( name: name, dependencies: deps, path: path, resources: resources ) } func testTarget(deps: [Target.Dependency] = []) -> Target { .testTarget( name: name, dependencies: deps, path: path ) } func product() -> Product { .library(name: name, targets: [name]) } } // MARK: - Helper to automatically generate the feature path and name for import extension Local { private var parsedDescription: (base: String, layer: String?) { let description = String(describing: self) guard let start = description.firstIndex(of: "("), let end = description.firstIndex(of: ")") else { return (description, nil) } let base = String(description[..<start]) var layer = String(description[description.index(after: start)..<end]) // remove type prefix like "Main.FeatureLayer." if let last = layer.split(separator: ".").last { layer = String(last).capitalizedFirst } return (base, layer) } } extension String { var capitalizedFirst: String { prefix(1).uppercased() + dropFirst() } }
Все что используется для настройки проекта, находится до "DSL PART"
Код демо-проекта
Если хотите попробовать DSL в реальном проекте:
P.S. Архитектура в каждой команде "немного" своя. Поэтому, пример в статье намеренно упрощён — чтобы было легче увидеть саму идею DSL.
Комментарии (7)

house2008
16.03.2026 08:21Писал тулу недавно для постройки графа SPM, обсмотрел тонну swifpm файлов, всегда бесило что там кто-то придумывает свой дсл и черт ногу сломит что там происходит) Хорошо есть
swift package dump-package --package-pathчтобы вернуть всё обратно в нормальный вид.
Даже в самом swiftpm пишут в простом понятном стиле https://github.com/swiftlang/swift-package-manager/blob/main/Package.swift
Спасибо за статью, но я бы это больше отнес к вредным советам, но дело вкуса конечно, никто не запретит вам так делать)

PALiarMo Автор
16.03.2026 08:21
Большое спасибо за фидбек. В статье не указал, но в xCode не требуется добавлять абсолютно все, большое спасибо что подсветили проблемы, изучу это, проанализирую. В продакшн проекте у меня 35 модулей, полет нормальный
MeGaPk
Не советую СПМ для более крупных проектов.
1) Если у вас есть Основная аппка и экстеншены, то СПМ будет тупо дублировать ресурсы под каждый экстеншен (https://www.emergetools.com/blog/posts/make-your-ios-app-smaller-with-dynamic-frameworks)
2) Вы описали удобные функции в этом Package.swift для DSL, это удобно, но если в будущем понадобиться создать новый Package.swift что бы туда что то перенести, то вам придется код DSL туда опять дублировать.
3) Если проект будет более большим, то будет бить в икскоде по производительности https://docs.tuist.dev/en/guides/features/projects/adoption/migrate/swift-package
Поэтому я бы посоветовал просто использовать Tuist для всего этого, и там можете описать DSL свой подход для удобства и спокойно проект создавать. Рекомендую юзать buildableFolders, что бы файлы появлялись автоматически в икскоде, удобно когда с ИИ используешь.
house2008
Вроде туист тут не обязателен, сейчас такое по дэфолту в Xcode новых проектах.
Но с посылом согласен, swiftpm самый худший менеджер созданный для разработки под iOS, но для небольших проектов на пару человек вполне пойдет по личным ощущениям.
MeGaPk
Tuist поможет избавиться от тыканья добавление зависимостей в самом икскоде, и избавит от конфликтов с pbproject (огромными файлами проекта). Ну и есть всякие удобные генераторы ресурсов что бы к ним обращаться напрямую. Ну и всякие прикольные штуки как кеширование внешних зависимостей, что бы каждый раз не собирать их в икскоде.