Monorepo with Swift: Code Organization for Beginners
When you start developing applications in Swift for iOS or macOS, sooner or later you face the question: how to organize code so that it doesn't turn into chaos? Especially if you have several projects (for example, an iOS app, a watchOS extension, and a server-side part on Vapor). One modern approach is a monorepo. In this article, we will break down what a monorepo is, why it is needed in Swift development, and how to organize it in practice.
What is a monorepo and why is it needed?
A monorepo is an approach to code management where all projects, libraries, and modules are stored in a single repository (for example, in Git). Unlike multi-repositories, where each project lives in its own separate storage, a monorepo unifies everything in one place.
For Swift developers, this provides several advantages:
- Single version of dependencies — you know exactly which version of a library is used across all projects.
- Simplified refactoring — changing an API in one library is immediately visible in all dependent projects.
- Shared settings — one SwiftLint, one CI/CD config, one set of formatting rules.
- Easy search — everything is in one place, no need to switch between repositories.
However, there are nuances: a monorepo requires discipline and a proper structure, otherwise it turns into a "dump." But for beginners, this is an excellent way to learn how to organize code.
Monorepo structure for Swift projects
Suppose you are creating an iOS app with shared modules. A typical monorepo structure in Swift might look like this:
MyMonorepo/├── Apps/│ ├── iOSApp/ # Main iOS application│ │ ├── Sources/│ │ ├── Resources/│ │ └── Package.swift│ └── macOSApp/ # macOS version│ ├── Sources/│ └── Package.swift├── Packages/│ ├── Core/ # Common utilities (networking, logging)│ │ ├── Sources/│ │ └── Package.swift│ ├── UIComponents/ # Reusable UI components│ │ ├── Sources/│ │ └── Package.swift│ └── Analytics/ # Analytics module│ ├── Sources/│ └── Package.swift├── Scripts/ # Helper scripts├── .swiftlint.yml # Unified linter config├── Package.swift # Root manifest (optional)└── README.mdNote: here we use Swift Package Manager (SPM) — the standard tool for managing dependencies in Swift. Each module is a separate Swift Package that can be connected to applications.
How to create a monorepo in Swift: step-by-step guide
Let's create a simple monorepo with two modules and one application. Follow these steps in the terminal:
Step 1: Create the root directory and initialize Git
mkdir MyMonorepocd MyMonorepogit initStep 2: Create the folder structure
mkdir -p Apps/iOSApp/Sourcesmkdir -p Packages/Core/Sourcesmkdir -p Packages/UIComponents/SourcesStep 3: Create Package.swift for the Core module
Create the file Packages/Core/Package.swift:
// swift-tools-version:5.9import PackageDescription
let package = Package( name: "Core", platforms: [ .iOS(.v16), .macOS(.v13) ], products: [ .library( name: "Core", targets: ["Core"]) ], targets: [ .target( name: "Core", path: "Sources") ])Step 4: Write simple code in the Core module
Create Packages/Core/Source