Monorepo with Swift: Code Organization in Practice
In modern iOS and macOS development, choosing a code management strategy is one of the key factors affecting scalability, delivery speed, and product quality. Traditionally, developers used multi-repositories (polyrepo), where each module or service lives in a separate Git repository. However, as projects and teams grow, the question increasingly arises: how to organize code so that it remains cohesive, reusable, and easily maintainable? The answer is a monorepo with Swift. In this article, we will explore what a monorepo is, why it is suitable for Swift projects, and how to organize it in practice using Swift Package Manager (SPM).
What is a Monorepo and Why is it Needed in Swift?
A monorepo is an approach where all the code of a project (or several related projects) is stored in a single repository. Unlike polyrepo, where each component is isolated, a monorepo unifies libraries, applications, tests, configurations, and documentation under a single version control.
For Swift developers, a monorepo is particularly relevant for several reasons:
- Single version of dependencies: You avoid version mismatch of libraries between different applications.
- Simplified refactoring: Changes to a shared module are immediately visible to all consumers within the repository.
- Fast integration: New features can be tested and released in a single pull request.
- Compatibility with SPM: Swift Package Manager is ideal for monorepos as it supports local packages and relative paths.
Monorepo Structure for a Swift Project
Let's consider a practical example. Suppose we have an iOS app (using SwiftUI) and a macOS app (using AppKit) that share common modules: a networking layer, authentication logic, and UI components. Let's organize the monorepo as follows:
MyMonorepo/├── Package.swift├── Sources/│ ├── AppiOS/│ │ └── main.swift│ ├── AppMacOS/│ │ └── main.swift│ ├── CoreNetworking/│ │ ├── Sources/│ │ │ └── HTTPClient.swift│ │ └── Tests/│ ├── CoreAuth/│ │ ├── Sources/│ │ │ └── AuthService.swift│ │ └── Tests/│ └── UIComponents/│ ├── Sources/│ │ └── CustomButton.swift│ └── Tests/├── Tests/│ ├── AppiOSTests/│ └── AppMacOSTests/├── .gitignore└── README.mdThe key file is Package.swift. It describes all modules and their dependencies. An example of such a file:
// swift-tools-version:5.9import PackageDescription
let package = Package( name: "MyMonorepo", platforms: [ .iOS(.v17), .macOS(.v14) ], products: [ .executable(name: "AppiOS", targets: ["AppiOS"]), .executable(name: "AppMacOS", targets: ["AppMacOS"]), .library(name: "CoreNetworking", targets: ["CoreNetworking"]), .library(name: "CoreAuth", targets: ["CoreAuth"]), .library(name: "UIComponents", targets: ["UIComponents"]) ], dependencies: [ // External dependencies, if needed // .package(url: "https://github.com/...", from: "1.0.0") ], targets: [ .executableTarget( name: "AppiOS", dependencies: ["CoreNetworking", "CoreAuth", "UIComponents"], path: "Sources/AppiOS" ), .executableTarget( name: "AppMacOS", dependencies: ["CoreNetworking", "CoreAuth", "UIComponents"], path: "Sources/AppMacOS" ), .target( name: "CoreNetworking", dependencies: [], path: "Sources/CoreNetworking/Sources" ), .target( name: "CoreAuth",