Монорепозиторий с Swift: организация кода на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Монорепозиторий с 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                

Блоги

Рекомендаций по книгам