1. Як повʼязані import і залежності targetʼів
Для новачків import у Swift часто здається чимось на кшталт «магічної мантри»: написали import Domain — і компілятор ніби зобовʼязаний усе зрозуміти та пробачити. На жаль, а інколи й на щастя, компілятор — істота злопамʼятна й принципова. Спочатку він хоче, щоб ви пояснили системі збирання, які модулі доступні цьому targetʼу, а вже потім — щоб ви підключали їх у вихідному коді.
У SwiftPM import — це не прохання «будь ласка, знайди мені модуль десь у Всесвіті». Це твердження: «цей файл компілюється в оточенні, де модуль X доступний». А ось чи доступний він, вирішує граф залежностей targetʼів, який ви описали в Package.swift. У документації SwiftPM це сформульовано дуже прямо: targets — це базові блоки пакета, і вони можуть залежати від інших targets та від products зовнішніх залежностей.
Модель «target → module → import»
Щоб не плутатися, зафіксуймо просту «сходинку», на якій тримається весь процес:
- Target — те, що SwiftPM компілює окремо.
- Зазвичай кожному target відповідає module (модуль).
- Якщо модуль доступний поточному targetʼу за залежностями, ви можете написати import <ModuleName>.
Дуже важливо: ви імпортуєте не «папку» і не «файл», а саме модуль. Тому сам факт, що щось «лежить десь поруч у Sources», нічого не гарантує.
Уявіть, що targets — це кімнати в офісі, а import — двері між ними. Package.swift вирішує, чи ці двері взагалі існують. А модифікатори доступу (public та інші) вирішують, чи вони відчинені, чи на них висить табличка «тільки для співробітників відділу». Про рівні доступу сьогодні скажемо рівно стільки, скільки потрібно, щоб не розгубитися без потреби.
Невелика схема:
flowchart LR
A["Sources/Domain/..."] -->|компілюється| D["Target Domain<br/>Модуль: Domain"]
B["Sources/Storage/..."] -->|компілюється| S["Target Storage<br/>Модуль: Storage"]
C["Sources/LibraryCLI/..."] -->|компілюється| L["Target LibraryCLI<br/>Модуль: LibraryCLI"]
L -->|"import Domain (якщо залежність є)"| D
S -->|"import Domain (якщо залежність є)"| D
2. Граф залежностей LibraryCLI: хто вище, хто нижче
Коли ми говоримо про напрям залежностей, то насправді маємо на увазі одне просте правило: верхній шар знає про нижній, а нижній про верхній — ні. І це не снобізм, а спосіб не втонути в циклічних залежностях та «спагеті-архітектурі».
Для нашого LibraryCLI логіка така: домен — у центрі, інфраструктура — навколо нього, а CLI-шар — зверху, бо саме він усе збирає докупи.
Граф в ідеалі виглядає так:
flowchart TD
LibraryCLI["LibraryCLI (виконуваний модуль)"] --> Domain["Domain"]
LibraryCLI --> Storage["Storage"]
LibraryCLI --> Networking["Networking"]
Storage --> Domain
Networking --> Domain
Зверніть увагу на відсутність стрілок назад. Якщо Domain почне залежати від Storage, ви отримаєте архітектурну «чорну діру»: домен почне знати про деталі зберігання, і за кілька тижнів ви вже писатимете щось на кшталт Domain/BookFileNameBuilder.swift (і так, це звучить так само сумно, як і виглядає).
3. Як залежності targetʼів дозволяють або забороняють import
Тепер до найпрактичнішого: коли саме import дозволений.
Розглянемо мінімальний фрагмент Package.swift, де ми задаємо залежності між targets. Цей код короткий, але саме він визначає долю всіх import у проєкті.
import PackageDescription
let package = Package(
name: "LibraryCLI",
targets: [
.executableTarget(
name: "LibraryCLI",
dependencies: ["Domain", "Storage", "Networking"]
),
.target(name: "Domain"),
.target(name: "Storage", dependencies: ["Domain"]),
.target(name: "Networking", dependencies: ["Domain"]),
]
)
Тепер читаємо це як правила імпорту:
- Усередині target LibraryCLI можна писати import Domain, import Storage, import Networking.
- Усередині target Storage можна писати import Domain, але не можна писати import LibraryCLI і не можна писати import Networking (якщо ми цього не вказали).
- Усередині target Domain не можна писати import Storage, import Networking, import LibraryCLI, тому що в Domain немає таких залежностей.
Саме звідси береться помилка рівня «No such module …» — SwiftPM просто не дає поточному targetʼу доступу до модуля.
4. Практика: що імпортуємо в кожному модулі
У цьому розділі ми подивимося, як мають виглядати файли в кожному targetʼі, щоб напрями залежностей було видно просто в коді.
Domain: жодних знань про інфраструктуру
У Domain ми тримаємо моделі та контракти. Наприклад, просту модель книги:
// Sources/Domain/Book.swift
import Foundation
public struct Book {
public let id: String
public var title: String
public init(id: String, title: String) {
self.id = id
self.title = title
}
}
Тут є два моменти, які часто дивують.
Перший момент: import Foundation не є SwiftPM-залежністю, це системний модуль, і його можна імпортувати «просто так» — якщо він потрібен.
Другий момент: чому саме public? Тому що Domain — окремий модуль. Якщо інший target, наприклад Storage, імпортує Domain, він побачить лише public оголошення. Усе, що без public, за замовчуванням має рівень доступу internal і залишається всередині модуля. Це не «ще одна бюрократія», а нормальна модульна капсула.
Storage: імпортуємо Domain, реалізуємо контракт
Припустімо, що в нас є репозиторій, який зберігає книги в памʼяті. Поки що без файлів — для прикладу це нормально:
// Sources/Storage/InMemoryBookRepository.swift
import Foundation
import Domain
public struct InMemoryBookRepository {
private var items: [Book] = []
public init() {}
public mutating func add(_ book: Book) {
items.append(book)
}
}
Тут важливо, що Storage залежить від Domain, отже import Domain є легальним. А зворотна залежність заборонена — і саме це тримає архітектуру в порядку.
LibraryCLI: імпортуємо все, але не перетворюємося на «смітник логіки»
Виконуваний target збирає все разом.
// Sources/LibraryCLI/main.swift
import Foundation
import Domain
import Storage
var repo = InMemoryBookRepository()
repo.add(Book(id: "1", title: "Swift для людей і котів"))
print("Додано книжок: 1") // Додано книжок: 1
CLI-шар має право імпортувати Storage, тому що це точка збирання. Але «мати право» не означає «треба тягнути сюди всю бізнес-логіку». У хорошому проєкті LibraryCLI часто виглядає як місце, де відбуваються парсинг команд, виклики сервісів і виведення результату, але не зберігання правил домену.
5. Помилки import: навіщо вони і як діагностувати
Чому помилка import — це корисний захист
Коли ви вперше бачите помилку на кшталт:
- No such module 'Domain'
- або Cannot find type 'Book' in scope (хоча тип точно десь є!)
зʼявляється відчуття, ніби Swift просто капризує. Насправді він робить рівно те, за що ми його цінуємо: не дає проєкту розвалитися.
Якби Domain раптом зміг імпортувати Storage, ви дуже швидко отримали б цикл:
- Domain імпортує Storage (бо «треба прочитати файл»),
- Storage імпортує Domain (бо «треба повернути Book»),
- і збирання зайде в глухий кут.
SwiftPM забороняє циклічні залежності targets, тому що неможливо зібрати модуль A, якщо йому потрібен модуль B, який, у свою чергу, вимагає модуль A ще до того, як A взагалі існує. Це як намагатися вдягнути шкарпетку поверх черевика, а черевик — поверх шкарпетки, не знімаючи їх із ноги.
Як зрозуміти, де саме проблема
Важлива навичка: за текстом помилки швидко зрозуміти, що саме треба виправити — Package.swift чи вихідні файли.
Якщо ви бачите “No such module X”, то найчастіше проблема саме в тому, що target, у якому лежить файл, не має залежності від модуля X у Package.swift.
Тобто алгоритм мислення тут такий: «У якому target лежить цей файл?» → «Чи є в цього target залежність від модуля, який я імпортую?» → «Чи збігається назва модуля з назвою targetʼа або productʼа?»
Якщо ж import проходить, але ви бачите “Cannot find type … in scope”, то часто це вже історія про рівні доступу: символ є в іншому модулі, але не public, тому зовні він ніби не існує.
Щоб краще запамʼяталося, ось невелика таблиця:
| Симптом | Часта причина | Де виправляти |
|---|---|---|
|
target не має залежності від |
|
при |
не |
вихідні файли |
після додавання залежностей |
конфлікт назв / перевантаження | частіше у вихідниках, іноді в дизайні модулів |
6. Зовнішні залежності: .product(...) і конфлікти модулів
Зовнішні пакети: чому import працює лише після .product(...)
Досі ми говорили про внутрішні targets. Але в SwiftPM є ще одна важлива гілка: зовнішні пакети.
Тут модель двокрокова:
- ви оголошуєте пакет у dependencies пакета, тобто кажете: «ми взагалі використовуємо цей зовнішній пакет»,
- ви додаєте конкретний product цього пакета до залежностей targetʼа, тобто кажете: «ось цей модуль доступний ось цьому targetʼу»,
- і лише потім у вихідних файлах зʼявляється право писати import <ModuleName>.
В офіційному гайді з SwiftPM це показано на прикладі підключення Figlet: target підключає product .product(name: "Figlet", package: "..."), після чого в коді можна зробити import Figlet.
Сильно спрощений приклад — без привʼязки до реального пакета:
import PackageDescription
let package = Package(
name: "LibraryCLI",
dependencies: [
.package(url: "https://example.com/cool-tools.git", from: "1.0.0")
],
targets: [
.executableTarget(
name: "LibraryCLI",
dependencies: [
.product(name: "CoolTools", package: "cool-tools")
]
)
]
)
І лише тоді в Sources/LibraryCLI/... стає доречним:
import CoolTools
Якщо ви оголосили пакет, але забули .product(...) у залежностях targetʼа, import не спрацює. Якщо ви додали .product(...), але помилилися в назві модуля, import теж не спрацює. Так, це саме та ситуація, коли «я все зробив, але воно не працює», і ви сидите з обличчям людини, яка натиснула кнопку ліфта двічі, щоб він приїхав швидше.
Конфлікт назв модулів і module aliasing
Іноді ви підключаєте два різні пакети, а в них збігаються назви модулів. Наприклад, обидва експортують модуль Utils. У такій ситуації import Utils стає неоднозначним, і SwiftPM або компілятор не можуть зрозуміти, що саме ви маєте на увазі.
Для таких випадків у SwiftPM є механізм module aliasing: ви можете задати «перейменування» модуля під час підключення product, щоб конфлікт зник. У proposal щодо module aliasing наводять приклад, де одному з Utils задають alias на кшталт GameUtils, щоб у вихідниках можна було явно імпортувати потрібний модуль.
Ми не будемо заглиблюватися в це — інструмент рідкісний, але корисний, — однак важливо знати: якщо ви раптом уперлися в конфлікт модулів, проблема не в import як такому, а в тому, що в проєкті зʼявилися два «однакові» імена.
7. Типові помилки при імпортах і залежностях targets
Помилка №1: писати import Domain, але не додати Domain до залежностей targetʼа.
Це найчастіша ситуація, і вона підступна тим, що начебто все очевидно: модуль же існує в пакеті. Але для SwiftPM існування модуля в пакеті не означає, що він доступний конкретному targetʼу. У результаті ви ловите No such module 'Domain' і починаєте підозрювати IDE у змові. Лікується це майже завжди перевіркою Package.swift: у targetʼа, де лежить файл, має бути залежність від Domain.
Помилка №2: переплутати назву пакета і назву модуля.
Зовнішній пакет може називатися одним способом (identity/URL), а модуль, який він експортує (product/target name), — іншим. Тоді ви чесно прописали .package(url: ...), але пишете import не того імені і отримуєте «No such module». Добра звичка — у Package.swift дивитися на .product(name: ...) — саме це і є назва модуля, яку ви будете імпортувати, як у прикладі з Figlet.
Помилка №3: забути про public і думати, що import «має відкрити все».
import підключає модуль, але не скасовує інкапсуляцію. Якщо ви оголосили struct Book без public у модулі Domain, то в модулі Storage цей тип буде невидимий — і ви отримаєте помилку Cannot find type 'Book' in scope, хоча імпорт формально працює. Це не баг і не «зайва суворість Swift», а нормальні правила модульної видимості.
Помилка №4: порушити напрям залежностей (наприклад, Domain імпортує Storage).
Іноді це починається невинно: «я просто хочу прочитати файл прямо з Domain». Але саме в цей момент архітектура починає розповзатися. Технічно SwiftPM може навіть не дати вам зробити це без явної залежності, але якщо ви все ж додасте її, далі дуже швидко зʼявляться цикли, дивні звʼязки й відчуття, що проєкт «пахне дротами». Якщо домену потрібна операція збереження, правильніше тримати в домені контракт (протокол), а реалізацію — у Storage, щоб стрілка й далі вела до домену.
Помилка №5: лікувати No such module додаванням import Foundation «про всяк випадок».
Іноді новачки бачать помилку імпорту й додають Foundation, бо «так часто роблять». Але Foundation тут ні до чого: проблема в графі залежностей targets або в неправильній назві модуля. Foundation — системний модуль; він не виправляє відсутність модульної залежності й не робить ваш import Domain раптом правильним.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ