Monorepo with Swift: Code Organization in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Monorepo with Swift: Code Organization in Practice

\n\n

Modern iOS development increasingly faces the challenge of scaling a codebase. When a project grows to dozens of modules, shared libraries, and multiple applications (iOS, macOS, watchOS), the traditional model with separate repositories (polyrepo) begins to slow down development. A monorepo comes to the rescue — a single repository containing the code of all ecosystem components. In this article, we will practically explore how to organize a monorepo for Swift projects using modern tools: Swift Package Manager (SPM), Tuist, and efficient CI/CD pipelines.

\n\n

1. Why Do You Need a Monorepo in the Swift Ecosystem?

\n\n

Before diving into implementation, it's important to understand the key advantages of this approach:

\n\n
    \n
  • Unified versioning — all modules and applications are in one commit, eliminating the "dependency hell" problem.
  • \n
  • Simplified refactoring — changing an API in a library is immediately visible to all consumers; the compiler highlights errors within a single PR.
  • \n
  • Transparent navigation — developers see the entire project code without switching between repositories.
  • \n
  • CI/CD optimization — you can configure builds for only changed modules (targeted builds), saving time and resources.
  • \n
\n\n

However, a monorepo requires discipline: a clear modular architecture and tools to prevent chaos. For Swift, the best choice today is Tuist — a utility that manages Xcode projects declaratively, or pure Swift Package Manager for a modular structure.

\n\n

2. Monorepo Structure with Swift Package Manager

\n\n

Let's start with a basic organization. Suppose we have two applications: MyAppiOS and MyAppMacOS, as well as a set of internal libraries. The file structure might look like this:

\n\n
Monorepo/\n├── Apps/\n│   ├── MyAppiOS/\n│   │   ├── Sources/\n│   │   └── Package.swift (or project.yml for Tuist)\n│   └── MyAppMacOS/\n│       ├── Sources/\n│       └── Package.swift\n├── Libraries/\n│   ├── Networking/\n│   │   ├── Sources/\n│   │   └── Package.swift\n│   ├── UIComponents/\n│   │   ├── Sources/\n│   │   └── Package.swift\n│   └── Core/\n│       ├── Sources/\n│       └── Package.swift\n├── Tests/\n│   ├── NetworkingTests/\n│   └── UIComponentsTests/\n├── Package.swift (root manifest)\n└── Tuist/ (if using Tuist)\n    └── Project.swift\n
\n\n

The root Package.swift ties everything together. Example manifest:

\n\n
// swift-tools-version:5.9\nimport PackageDescription\n\nlet package = Package(\n    name: "MyMonorepo",\n    platforms: [\n        .iOS(.v16), .macOS(.v13)\n    ],\n    products: [\n        .library(name: "Networking", targets: ["Networking"]),\n        .library(name: "UIComponents", targets: ["UIComponents"]),\n        .library(name: "Core", targets: ["Core"]),\n        .executable(name: "MyAppiOS", targets: ["MyAppiOS"]),\n        .executable(name: "MyAppMacOS", targets: ["MyAppMacOS"])\n    ],\n    dependencies: [\n        // External dependencies\n        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")\n    ],\n    targets: [\n        .target(\n            name: "Networking",\n            dependencies: ["Core", .product(name: "Alamofire", package: "Alamofire")],\n            path: "Libraries/Networking/Sources"\n        ),\n        .target(\n            name: "UIComponents",\n            dependencies: ["Core"],\n            path: "Libraries/UIComponents/Sources"\n        ),\n        .target(\n            name: "Core",\n            path: "Libraries/Core/Sources"\n        ),\n        .executableTarget(\n            name: "MyAppiOS",\n            dependencies: ["Networking", "UIComponents"],\n            path: "Apps/MyAppiOS/Sources"\n                

Blogs

Book Recommendations