Легковесная сетевая библиотека с поддержкой Swift 6, разработанная для современных iOS-приложений с использованием async/await, чистой архитектуры и тестируемых абстракций.
🌍 Язык · English · Español · Português (Brasil) · 日本語 · 简体中文 · 한국어 · Русский
- ✅ Нативный
async/awaitAPI - ✅ Сетевой слой на основе протоколов с полной поддержкой моков
- ✅ Типизированное декодирование запросов / ответов
- ✅ Совместимость с Swift 6 + Swift Concurrency
- ✅ Спроектирован для MVVM / Clean Architecture
- ✅ Ноль сторонних зависимостей
- ✅ Встроенные Transport'ы с предустановленными ответами для тестирования
- ✅ Повторные попытки с экспоненциальной задержкой и джиттером
- ✅ Прозрачное обновление токенов и повторная аутентификация
- ✅ Условный GET через ETag / If-None-Match / 304
💬 Присоединяйтесь к обсуждению. Обратная связь и вопросы приветствуются
В этом репозитории содержится запускаемое демо-приложение на SwiftUI с локальной ссылкой на пакет.
- Клонируйте репозиторий:
git clone https://github.com/gentle-giraffe-apps/GentleNetworking.git
- Откройте демо-проект:
Demo/GentleNetworkingDemo/GentleNetworkingDemo.xcodeproj
- Выберите симулятор с iOS 17+.
- Соберите и запустите (⌘R).
Проект предварительно настроен с локальной ссылкой на Swift Package GentleNetworking и должен работать без дополнительной настройки.
- Откройте ваш проект в Xcode
- Перейдите в File → Add Packages...
- Введите URL репозитория:
https://github.com/gentle-giraffe-apps/GentleNetworking.git - Выберите правило версионирования (или
mainво время разработки) - Добавьте продукт GentleNetworking в целевой модуль вашего приложения
Добавьте зависимость в ваш Package.swift:
dependencies: [
.package(url: "https://github.com/gentle-giraffe-apps/GentleNetworking.git", from: "1.0.0")
]Затем добавьте "GentleNetworking" в нужный целевой модуль:
.target(
name: "YourApp",
dependencies: ["GentleNetworking"]
)Этот проект обеспечивает контроль качества через CI и статический анализ:
- CI: Все коммиты в
mainдолжны пройти проверки GitHub Actions - Статический анализ: DeepSource запускается при каждом коммите в
main. Бейдж показывает текущее количество нерешённых проблем статического анализа. - Покрытие тестами: Codecov отчитывается о покрытии строк для ветки
main
Эти проверки предназначены для безопасного развития системы дизайна с течением времени.
GentleNetworking построен вокруг единого протокол-ориентированного HTTPNetworkService, который координирует запросы с помощью внедрённых абстракций эндпоинтов, окружения и аутентификации.
flowchart TB
HTTP["HTTPNetworkService<br/><br/>- request(...)"]
Endpoint["EndpointProtocol<br/><br/><br/>"]
Env["APIEnvironmentProtocol<br/><br/><br/>"]
Auth["AuthServiceProtocol<br/><br/><br/>"]
HTTP --> Endpoint
HTTP --> Env
HTTP -->|внедрён| Auth
flowchart TB
APIEndpoint["APIEndpoint enum<br/><br/>case endpoint1<br/>…<br/>endpointN"]
EndpointProtocol["EndpointProtocol<br/><br/>- path<br/>- method<br/>- query<br/>- body<br/>- requiresAuth"]
APIEndpoint -->|соответствует| EndpointProtocol
import GentleNetworking
let apiEnvironment = DefaultAPIEnvironment(
baseURL: URL(string: "https://api.company.com")
)
nonisolated enum APIEndpoint: EndpointProtocol {
case signIn(username: String, password: String)
case model(id: Int)
case models
var path: String {
switch self {
case .signIn: "/api/signIn"
case .model(let id): "/api/model/\(id)"
case .models: "/api/models"
}
}
var method: HTTPMethod {
switch self {
case .signIn: .post
case .model, .models: .get
}
}
var query: [URLQueryItem]? {
switch self {
case .signIn, .model, .models: nil
}
}
var body: [String: EndpointAnyEncodable]? {
switch self {
case .signIn(let username, let password): [
"username": EndpointAnyEncodable(username),
"password": EndpointAnyEncodable(password)
]
case .model, .models: nil
}
}
var requiresAuth: Bool {
switch self {
case .model, .models: true
case .signIn(username: _, password: _): false
}
}
}let networkService = HTTPNetworkService()SystemKeyChainAuthService — это встроенная реализация AuthServiceProtocol. Она сохраняет Bearer-токен в системном keychain и автоматически прикрепляет его к запросам эндпоинтов, где requiresAuth равен true.
let keyChainAuthService = SystemKeyChainAuthService()
struct AuthTokenModel: Decodable, Sendable {
let token: String
}
let authTokenModel: AuthTokenModel = try await networkService.request(
to: .signIn(username: "user", password: "pass"),
via: apiEnvironment
)
try await keyChainAuthService.saveAccessToken(
authTokenModel.token
)Используйте request для декодирования одного объекта из ответа:
struct Model: Decodable, Sendable {
let id: Int
let property: String
}
let model: Model = try await networkService.request(
to: .model(id: 123),
via: apiEnvironment
)Используйте requestModels для декодирования массива объектов из ответа:
let models: [Model] = try await networkService.requestModels(
to: .models,
via: apiEnvironment
)GentleNetworking предоставляет абстракцию транспортного слоя для удобного мокирования в тестах.
Возвращает фиксированный ответ на любой запрос:
let transport = CannedResponseTransport(
string: #"{"id": 1, "title": "Test"}"#,
statusCode: 200
)
let networkService = HTTPNetworkService(transport: transport)Сопоставляет запросы по методу и шаблону пути для более реалистичных тестовых сценариев:
let transport = CannedRoutesTransport(routes: [
CannedRoute(
pattern: RequestPattern(method: .get, path: "/api/models"),
response: CannedResponse(string: #"[{"id": 1}]"#)
),
CannedRoute(
pattern: RequestPattern(method: .post, pathRegex: "^/api/model/\\d+$"),
response: CannedResponse(string: #"{"success": true}"#)
)
])
let networkService = HTTPNetworkService(transport: transport)GentleNetworking использует App Transport Security (ATS) от Apple для защиты транспортного уровня — TLS 1.2+, проверка сертификатов, прямая секретность — всё это обеспечивается операционной системой и включено по умолчанию.
Для приложений с повышенными требованиями безопасности используйте встроенный PinningTransport с закреплением открытого ключа или сертификата:
import CryptoKit
// Закрепление открытого ключа (рекомендуется — работает после обновления сертификата)
let service = HTTPNetworkService(
transport: PinningTransport(
pinnedDomains: [
"api.example.com": PublicKeyPinningEvaluator(
pinnedKeyHashes: [primaryKeyHash, backupKeyHash]
)
]
)
)
// Закрепление сертификата (проще, но перестаёт работать при каждом обновлении)
let service = HTTPNetworkService(
transport: PinningTransport(
pinnedDomains: [
"api.example.com": CertificatePinningEvaluator(
pinnedCertificates: [certDERData]
)
]
)
)Домены без закрепления используют стандартную проверку ATS. Реализуйте ServerTrustEvaluator для пользовательской логики доверия.
Полное руководство, включая лучшие практики, пользовательские оценщики и альтернативные подходы, см. в SECURITY.md.
GentleNetworking предоставляет компонуемые обёртки транспорта для логики повторных попыток и автоматического обновления токенов. Поскольку это транспорты, они стекируются друг с другом и с PinningTransport.
Повторяет неудачные запросы с экспоненциальной задержкой и настраиваемым джиттером. По умолчанию повторяет при 429, 500, 503 и сетевых ошибках — никогда при 401 или других клиентских ошибках.
let service = HTTPNetworkService(
transport: RetryTransport(
inner: URLSessionTransport(session: .shared),
policy: RetryPolicy(
maxRetries: 3,
baseDelay: 0.5,
maxDelay: 30.0,
jitter: .full // .full | .equal | .decorrelated
)
)
)Перехватывает ответы HTTP 401, обновляет токен через замыкание, предоставленное вызывающей стороной, повторно авторизует исходный запрос и повторяет его один раз. Параллельные 401 сериализуются, чтобы выполнялось только одно обновление.
let service = HTTPNetworkService(
transport: ReauthTransport(
inner: RetryTransport(),
authService: keyChainAuthService,
refreshToken: {
// 1. Call your refresh endpoint to obtain a new access credential
// 2. Save the new credential via authService so future requests use it
}
)
)Позволяет избежать повторной загрузки дорогостоящих неизменённых ресурсов. При первом GET заголовок ETag сервера и тело ответа кэшируются. Последующие GET отправляют If-None-Match; если сервер отвечает 304 Not Modified, возвращается кэшированное тело без повторной передачи данных.
let service = HTTPNetworkService(
transport: ETagTransport(
inner: URLSessionTransport(session: .shared)
)
)Внедрите пользовательский ETagStoreProtocol для хранения на диске или в базе данных:
let service = HTTPNetworkService(
transport: ETagTransport(
inner: URLSessionTransport(session: .shared),
store: MyDiskETagStore()
)
)Разместите ReauthTransport снаружи, RetryTransport посередине, а ETagTransport внутри:
ReauthTransport ← перехватывает 401 после исчерпания попыток
└─ RetryTransport ← повторяет 429/500/503 с задержкой + джиттер
└─ ETagTransport ← условный GET через ETag / 304
└─ URLSessionTransport (или PinningTransport)RetryTransport уже пропускает 401 (defaultShouldRetry возвращает false), поэтому передаёт ошибки аутентификации напрямую в ReauthTransport, не расходуя попытки. ETagTransport находится внутри retry, чтобы повторные запросы также использовали кэш.
GentleNetworking построен на следующих принципах:
- ✅ Предсказуемость вместо магии
- ✅ Протокол-ориентированный дизайн
- ✅ Явное внедрение зависимостей
- ✅ Современная конкурентность Swift
- ✅ Тестируемость по умолчанию
- ✅ Малая площадь API с надёжными гарантиями
Библиотека намеренно минималистична и избегает чрезмерной абстракции или сокрытия сетевого поведения.
Часть черновой работы и редакторской доработки в этом репозитории была ускорена с помощью больших языковых моделей (включая ChatGPT, Claude и Gemini) при непосредственном контроле, валидации и финальном утверждении человеком. Все технические решения, код и архитектурные выводы написаны и проверены мейнтейнером репозитория.
Лицензия MIT Бесплатно для личного и коммерческого использования.
Создано Jonathan Ritchey Gentle Giraffe Apps Senior iOS Engineer --- Swift | SwiftUI | Concurrency