Monorepo with Swift: organizing code for iOS and macOS projects
In modern iOS and macOS development, the choice of code management strategy directly affects development speed, refactoring quality, and team collaboration convenience. The traditional approach with many separate repositories (polyrepo) often leads to dependency duplication, version synchronization difficulties, and codebase fragmentation. An alternative is a monorepo — a single storage for all modules, libraries, and project applications. In this article, using Swift and the Apple ecosystem as an example, we will analyze how to organize a monorepo, which tools to use, and what pitfalls to consider.
What is a monorepo and why is it needed in Swift projects
A monorepo is a version control strategy where all project code (multiple applications, libraries, configurations) is stored in a single Git repository. In the context of Swift, this means you can have in one repository:
- iOS application (e.g.,
MyApp) - macOS application (
MyMacApp) - shared model library (
CoreModels) - networking layer (
Networking) - UI components (
UIComponents) - tests and CI scripts
Advantages of a monorepo for Swift projects:
- Single version of dependencies — all modules use the same version of a library (e.g.,
AlamofireorSwiftUI), which eliminates conflicts. - Atomic commits — changes to a library's API and its usage in the application are committed in a single commit, simplifying code review and rollback.
- Simplified refactoring — you can rename a public method in a module and immediately fix all call sites in the same repository.
- Shared CI/CD — one pipeline for building and testing all components.
Tools for working with a monorepo in Swift
In the Apple ecosystem, there are two main tools for organizing a monorepo: Swift Package Manager (SPM) and Tuist. Let's look at their features.
Swift Package Manager (SPM)
SPM is a built-in package manager that is well-suited for monorepos. You define modules via Package.swift, and Xcode automatically resolves dependencies. Example structure:
MyMonorepo/├── Package.swift├── Sources/│ ├── CoreModels/│ │ └── Models.swift│ ├── Networking/│ │ └── APIClient.swift│ └── UIComponents/│ └── ButtonView.swift├── Apps/│ ├── MyApp/│ │ └── (iOS project)│ └── MyMacApp/│ └── (macOS project)└── Tests/ ├── CoreModelsTests/ └── NetworkingTests/Example Package.swift for such a monorepo:
// swift-tools-version:5.9import PackageDescription
let package = Package( name: "MyMonorepo", platforms: [ .iOS(.v16), .macOS(.v13) ], products: [ .library(name: "CoreModels", targets: ["CoreModels"]), .library(name: "Networking", targets: ["Networking"]), .library(name: "UIComponents", targets: ["UIComponents"]), ], dependencies: [ .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0") ], targets: [ .target(name: "CoreModels"), .target( name: "Networking", dependencies: ["CoreModels", "Alamofire"] ), .target( name: "UIComponents", dependencies: ["CoreModels"] ), .testTarget(name: "CoreModelsTests", dependencies: ["CoreModels"]), .testTarget(name: "NetworkingTests", dependencies: ["Networking"]), ])