Монорепозиторий с Swift: организация кода на практике
В современной iOS и macOS разработке выбор стратегии управления кодом — один из ключевых факторов, влияющих на масштабируемость, скорость поставки и качество продукта. Традиционно разработчики использовали мультирепозитории (polyrepo), где каждый модуль или сервис живет в отдельном Git-репозитории. Однако с ростом проектов и команд все чаще встает вопрос: как организовать код так, чтобы он оставался целостным, переиспользуемым и легко поддерживаемым? Ответ — монорепозиторий (monorepo) с Swift. В этой статье мы разберем, что такое монорепозиторий, почему он подходит для Swift-проектов, и как организовать его на практике с помощью Swift Package Manager (SPM).
Что такое монорепозиторий и зачем он нужен в Swift?
Монорепозиторий — это подход, при котором весь код проекта (или нескольких связанных проектов) хранится в одном репозитории. В отличие от polyrepo, где каждый компонент изолирован, monorepo объединяет библиотеки, приложения, тесты, конфигурации и документацию под единой версией контроля.
Для Swift-разработчиков монорепозиторий особенно актуален по нескольким причинам:
- Единая версия зависимостей: Вы избегаете рассинхронизации версий библиотек между разными приложениями.
- Упрощение рефакторинга: Изменения в общем модуле сразу видны всем потребителям внутри репозитория.
- Быстрая интеграция: Новые фичи можно тестировать и выпускать в одном пулл-реквесте.
- Совместимость с SPM: Swift Package Manager идеально подходит для монорепозиториев, так как поддерживает локальные пакеты и относительные пути.
Структура монорепозитория для Swift-проекта
Рассмотрим практический пример. Предположим, у нас есть приложение для iOS (на SwiftUI) и macOS (на AppKit), которые используют общие модули: сетевой слой, логику авторизации и UI-компоненты. Организуем монорепозиторий следующим образом:
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.md
Ключевой файл — Package.swift. Он описывает все модули и их зависимости. Пример такого файла:
// swift-tools-version:5.9
import 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: [
// Внешние зависимости, если нужны
// .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",
dependencies: ["CoreNetworking"],
path: "Sources/CoreAuth/Sources"
),
.target(
name: "UIComponents",
dependencies: [],
path: "Sources/UIComponents/Sources"
),
// Тесты
.testTarget(
name: "CoreNetworkingTests",
dependencies: ["CoreNetworking"],
path: "Sources/CoreNetworking/Tests"
),
.testTarget(
name: "CoreAuthTests",
dependencies: ["CoreAuth"],
path: "Sources/CoreAuth/Tests"
)
]
)
Практические примеры кода
Рассмотрим, как выглядят модули внутри монорепозитория на Swift.
Модуль CoreNetworking (HTTPClient.swift):
import Foundation
public protocol HTTPClient {
func get(url: URL) async throws -> Data
}
public class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
public func get(url: URL) async throws -> Data {
let (data, _) = try await session.data(from: url)
return data
}
}
Модуль CoreAuth (AuthService.swift):
import Foundation
import CoreNetworking
public class AuthService {
private let client: HTTPClient
public init(client: HTTPClient) {
self.client = client
}
public func login(username: String, password: String) async throws -> Bool {
let url = URL(string: "https://api.example.com/login")!
let body = ["username": username, "password": password]
let jsonData = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: .init(url: url))
let response = try JSONDecoder().decode([String: Bool].self, from: data)
return response["success"] ?? false
}
}
Модуль UIComponents (CustomButton.swift):
import SwiftUI
public struct CustomButton: View {
let title: String
let action: () -> Void
public init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
public var body: some View {
Button(action: action) {
Text(title)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
Приложение AppiOS (main.swift):
import SwiftUI
import CoreAuth
import UIComponents
struct ContentView: View {
@State private var isLoggedIn = false