Leveraging Swift Package Manager to Maintain Clean Architecture Layers in iOS Development — Part 1
Maintaining clean and scalable architecture is crucial for ensuring long-term code maintainability and reducing technical debt. One common challenge is accidentally creating unwanted dependencies between layers (Data, Domain, Presentation) in a Clean Architecture setup.
So this is why Swift Package Manager useful to enforce separation of concerns and prevent layers from inadvertently referencing each other. By modularizing your app into SPM packages, you can create clear boundaries and improve the quality of your codebase.
Why Use Swift Package Manager for Modularization?
Swift Package Manager is a powerful tool for managing dependencies, and it’s natively integrated into Xcode. By breaking down your app into separate SPM packages, you can achieve:
- Enforced Boundaries: Prevent unintentional imports by restricting dependencies.
- Reusability: Make layers independent and reusable across projects.
- Faster Builds: Smaller modules reduce build times.
Setting Up the Layers with SPM
Create Separate Packages for Each Layer
- Data Layer: Handles networking, database access, and data manipulation.
- Domain Layer: Contains business logic, entities, and use cases.
- Presentation Layer: Manages UI components and user interactions.
RootProject
│
├── Packages
│ ├── DataLayer
│ ├── DomainLayer
│ ├── PresentationLayer
│
├── App (Main Target)
Define Dependencies Explicitly
In the Package.swift
file for each layer, only define the dependencies that are absolutely necessary.
let package = Package(
name: "DataLayer",
platforms: [
.iOS(.v14),
.macOS(.v12)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "DataLayer",
targets: ["DataLayer"]),
],
dependencies: [
.package(path: "../DomainLayer")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "DataLayer",
dependencies: [
"DomainLayer"
]
),
.testTarget(
name: "DataLayerTests",
dependencies: ["DataLayer"]
),
]
)
let package = Package(
name: "DomainLayer",
platforms: [
.iOS(.v14),
.macOS(.v12)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "DomainLayer",
targets: ["DomainLayer"]),
],
dependencies: [
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "DomainLayer",
dependencies: [
]
),
.testTarget(
name: "DomainLayerTests",
dependencies: ["DomainLayer"]
),
]
)
let package = Package(
name: "PresentationLayer",
platforms: [
.iOS(.v14),
.macOS(.v12)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "PresentationLayer",
targets: ["PresentationLayer"]),
],
dependencies: [
.package(path: "../DomainLayer")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "PresentationLayer",
dependencies: [
"DomainLayer"
]
),
.testTarget(
name: "PresentationLayerTests",
dependencies: ["PresentationLayer"]
),
]
)
Preventing Circular Dependencies
SPM will throw an error if you attempt to create circular dependencies, which is great for keeping your architecture clean. For example:
Wrong:
- Presentation importing DataLayer and vice versa.
Correct:
- Presentation imports DomainLayer for use cases
- DataLayer does not depend on Presentation
- Domain Layer not depend to DataLayer or PresentationLayer
Integrating into the Main App
Add the SPM packages as dependencies in your app target. You can do this by going to your project settings, selecting the app target, and linking the appropriate layers under “Frameworks, Libraries, and Embedded Content.”
By modularizing your Clean Architecture layers with Swift Package Manager, you can enforce a strict separation of concerns, reduce build times, and create a more maintainable codebase. Whether you’re working on a small app or a large-scale project, this approach sets the foundation for clean and efficient development.