Architecture
Modularization
What I extracted into Swift Packages and what stayed in the app target.
What I pulled into Packages, and what I left in one target on purpose.
Part of Blueprint lives in local Swift Packages under Packages/. The rest stays in the blueprint app target.
| Package | Exports | Imports |
|---|---|---|
| DesignSystem | DSSpacing, DSTypography, DSRadius, DSColor, skeleton modifiers |
Nothing from app |
| Networking | NetworkClient, URLSessionNetworkClient, NetworkError |
Foundation only |
Views, ViewModels, UseCases, Repositories, DI, and Navigation remain in the app target.
Why I used packages at all
Boundaries. Networking cannot accidentally import SwiftUI. DesignSystem cannot reference ViewModels.
Reuse. Another target could depend on Networking without copying files.
Compile isolation. Small packages rebuild faster when I touch unrelated app code.
Learning. Modularization was its own chapter; I wanted the repo to show a split early, even for a small app.
What I did
Two packages: visual tokens + HTTP transport. Everything feature-specific stays in the app.
Why (then)
These were the only pieces that felt genuinely reusable and framework-free. Geoapify DTOs belong to this app, not a generic library.
What Iād reconsider
If compile time or team size grew, I would extract Domain, then feature modules. Today one target is faster to open and debug than five local packages.
Why I did not modularize features
Common next steps in bigger codebases:
| Module | What it would contain |
|---|---|
HomeFeature |
HomeView, HomeViewModel, HomeFactory |
DetailFeature |
Detail screen + factory |
FavoritesFeature |
Favorites tab + SwiftData wiring |
Domain package |
Entities + UseCase protocols |
Data package |
Repositories, DTOs, mappers |
I did not do that here.
App size. Two tabs, one pushed screen. Feature modules pay off with parallel teams, slow builds, or optional binaries.
Navigation and DI cross cuts. AppRoute references POI everywhere. DIContainer wires all tabs. Splitting features means deciding who owns routes and factories up front. Solvable, extra ceremony for a study app.
Thin Domain/Data. A handful of entities and use cases. A Domain package shines with widgets, extensions, or multiple targets. Discover has one iOS app.
I optimized for learning layer boundaries inside one target first. Feature modules feel like a later chapter, not a prerequisite for MVVM or repositories.
When I would split features
- Compile time hurts on every View tweak
- Two people conflict daily on the same target
- A feature ships or A/B tests independently
- A widget or App Clip needs Home without Favorites
Rough target layout:
Packages/
DesignSystem/
Networking/
Domain/
Data/
Navigation/
App/
DiscoverApp/ @main, DIContainer, TabView
HomeFeature/
DetailFeature/
FavoritesFeature/
Each feature depends on Domain and Navigation, not sibling features. The app target composes them in DIContainer.
DesignSystem
Shared tokens so screens do not hardcode 16 or .blue:
- Spacing (
DSSpacing.md) - Typography + Dynamic Type
- Category colors for POI cards
- Skeleton shimmer for loading
Presentation imports DesignSystem. The package knows nothing about POIs.
Networking package
HTTP abstraction only. Geoapify DTOs stay in app Data. See Networking for why URL building also stayed in Repositories.
Could everything stay in one target?
Yes. Many study apps do. I extracted DesignSystem and Networking to practice Package boundaries without the cost of feature modules.
Adding a package
- File ā New ā Package in Xcode (or folder +
Package.swift) - Add local package dependency to
blueprinttarget - Keep Domain entities out of packages unless multiple targets need them